Mert Özen Further With Every Line
SEO & GEO GEO (Generative Engine Optimization) — #06

Building Sites for AI Agents: Semantic HTML, the Accessibility Tree and Lighthouse Agentic Browsing

Mert Özen Nov 8, 2026 9 min 4 views
Building Sites for AI Agents: Semantic HTML, the Accessibility Tree and Lighthouse Agentic Browsing

Browser agents read a page through screenshots, the DOM and the accessibility tree; a button built from a div doesn't show up there at all, and a button without a name doesn't say what it does. We measure two versions of an Angular 22 form with an accessibility tree dump and Lighthouse 13.6.0's Agentic Browsing category, showing what the tool catches, what it misses and WebMCP's experimental status.

Version note: This post is based on the "agentic experiences" section of Google's AI (Artificial Intelligence) optimization guide and Chrome for Developers' June 2026 post on the agent-ready toolkit; the code examples were tested with Angular 22.2, Lighthouse 13.6.0 and Chrome 156.

Guess First

Bookstore's "track my order" page has an order number box, a search button with a magnifier icon, and a div labeled "Clear" that empties the box when clicked. A user tells their AI assistant "check my order 4711 at Bookstore", and the assistant's browser agent opens the page. Which elements do you think the agent sees as something clickable? Does Lighthouse's new Agentic Browsing category catch every problem on this page?

How Does a Browser Agent Read a Page?

The "Explore agentic experiences" section of Google's AI optimization guide defines agents as autonomous systems that carry out tasks for users, such as making reservations or comparing product specifications. According to the guide, browser agents can understand a page in three ways: by analyzing screenshots, by inspecting the DOM (Document Object Model) and by reading the accessibility tree. The guide's recommendation is semantic HTML (HyperText Markup Language), and its reasoning is notable: semantic HTML helps screen readers parse and navigate the page more easily.

That sentence is actually the most solid thing you can say about agents on the GEO (Generative Engine Optimization) side: there's no need to wait for a new standard, because the structure a person using a screen reader needs and the structure an agent needs are largely the same. The Chrome team's agent-ready toolkit post says the same thing in different words: accessibility is for humans first.

We covered the agent concept itself, tool use and the agent loop in the AI Agents and MCP (Model Context Protocol) series; this post is only about how the site looks to an agent.

The Accessibility Tree: The Page the Agent Sees

I wrote two versions of Bookstore's order tracking form in Angular 22. First, the common version:

@Component({
  selector: 'app-order-lookup-bad',
  template: `
    <div class="search">
      <input type="text" placeholder="Order number" (input)="orderNumber.set($any($event.target).value)">
      <button type="button" (click)="lookup()"><span class="icon-search"></span></button>
      <div class="link" (click)="clear()">Clear</div>
    </div>
  `,
})
export class OrderLookupBad {
  readonly orderNumber = signal('');
  lookup(): void {}
  clear(): void {}
}

Then the semantic version:

@Component({
  selector: 'app-order-lookup',
  template: `
    <form (submit)="lookup($event)">
      <label for="order-number">Order number</label>
      <input id="order-number" name="orderNumber" type="text" required
             (input)="orderNumber.set($any($event.target).value)">
      <button type="submit">Find my order</button>
    </form>
    <p role="status">{{ status() }}</p>
  `,
})
export class OrderLookup {
  readonly orderNumber = signal('');
  readonly status = signal('');

  lookup(event: Event): void {
    event.preventDefault();
    this.status.set(`Looking up order ${this.orderNumber()}`);
  }
}

I rendered both components on the server with @angular/platform-server, put them among Bookstore's static files, and wrote a small script that dumps Chrome's accessibility tree with puppeteer-core:

import * as chromeLauncher from 'chrome-launcher';
import puppeteer from 'puppeteer-core';

const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless=new', '--no-first-run'] });
const browser = await puppeteer.connect({ browserURL: `http://127.0.0.1:${chrome.port}` });
const page = await browser.newPage();
for (const name of ['bad', 'good']) {
  await page.goto(`http://localhost:5481/agents/${name}.html`);
  const tree = await page.accessibility.snapshot({ interestingOnly: true });
  const lines = [];
  const walk = (node, depth) => {
    lines.push(`${'  '.repeat(depth)}${node.role}${node.name ? ` "${node.name}"` : ''}`);
    for (const child of node.children ?? []) walk(child, depth + 1);
  };
  walk(tree, 0);
  console.log(`--- ${name}.html\n${lines.join('\n')}`);
}
await browser.disconnect();
await chrome.kill();
--- bad.html
RootWebArea "Track your order | Bookstore"
  textbox "Order number"
  button
  StaticText "Clear"
--- good.html
RootWebArea "Track your order | Bookstore"
  form
    StaticText "Order number"
    textbox "Order number"
    button "Find my order"
  status

The answer to the first opening question is in this output. In the bad version, the agent sees a button but it has no name; it can only guess what it does by interpreting the icon in a screenshot. "Clear" isn't even a button: in the accessibility tree it's just a StaticText. Nothing says it's clickable. In the good version there's a form, a textbox with a name, a button that says what it does, and a status region where the result will be written.

button versus div, label versus placeholder

The reason for the difference is the browser's built-in semantics. A <button> comes with a role, keyboard focus and Enter/Space behavior out of the box; the text inside it or an aria-label gives it an accessible name. A <div (click)>, on the other hand, only adds an event listener in JavaScript; the browser, and therefore the accessibility tree, doesn't know it's a control. Angular's (click) binding does nothing magical in that respect.

The same distinction applies between navigation and action. Everything that takes you to another page should be an <a href>, everything that does something on the page a <button>. From that distinction the agent understands that it can follow a link, while pressing a button will change something; it's the same logic as Google's crawler only counting <a> elements with an href as links.

There are two ways to name an icon button: visible text or aria-label="Search orders". Visible text is clearer for both humans and agents; if the icon is required, aria-label is a must.

On the input side, an interesting detail shows up in the output: the textbox in the bad version also got the name "Order number". When there's no label, the browser can use the placeholder as the accessible name. That's a fallback, not a solution: the placeholder disappears once you start typing, and many guides recommend not relying on it alone. Associating the input with <label for> makes the name permanent and explicit. The form and type="submit" give the agent the information "these fields are submitted together", and browser behaviors such as submitting the form with Enter come for free.

Lighthouse's Agentic Browsing Category

The Chrome team announced the agent-ready toolkit on June 22, 2026; the most concrete tool in it is Lighthouse's Agentic Browsing category. According to the announcement, the category is informational and not meant for benchmarking: instead of the classic 0–100 score it gives pass/fail states and warnings; the category's own description also says it's still under development and subject to change. I looked at the source code of lighthouse 13.6.0 on npm; the category contains these audits:

  • agent-accessibility-tree: a subset of axe rules selected for agents, including button-name, label, link-name, document-title and a set of ARIA (Accessible Rich Internet Applications) rules.
  • cumulative-layout-shift: elements moving after render, that is, CLS (Cumulative Layout Shift). A button that shifts just as the agent is about to click means clicking the wrong place.
  • llms-txt: the llms.txt check we saw in the fifth post of the series.
  • webmcp-registered-tools, webmcp-form-coverage and webmcp-schema-validity: WebMCP integration.
  • ard-schema: validity of an ARD (Agentic Resource Discovery) manifest served at /.well-known/ard.json or linked with rel="ard".

The category's score is in "fraction" mode: how many of the applicable audits passed.

Measuring the Bookstore Form with Lighthouse

I ran both pages on local Chrome 156 with only this category:

npx lighthouse http://localhost:5481/agents/bad.html --only-categories=agentic-browsing --output=json --output-path=agentic-bad.json

I extracted the important lines of the JSON (JavaScript Object Notation) reports with a short script; the results:

agentic-bad.json category score 0.67
  agent-accessibility-tree     score=0     mode=binary
      Buttons must have discernible text: body > app-order-lookup-bad > div.search > button
  webmcp-form-coverage         score=None  mode=notApplicable
  webmcp-registered-tools      score=None  mode=notApplicable
  webmcp-schema-validity       score=None  mode=notApplicable
  cumulative-layout-shift      score=1     mode=numeric        0
  llms-txt                     score=1     mode=binary
  ard-schema                   score=None  mode=notApplicable
agentic-good.json category score 1
  agent-accessibility-tree     score=1     mode=binary         All audits passed
  webmcp-form-coverage         score=None  mode=notApplicable
  webmcp-registered-tools      score=None  mode=notApplicable
  webmcp-schema-validity       score=None  mode=notApplicable
  cumulative-layout-shift      score=1     mode=numeric        0
  llms-txt                     score=1     mode=binary
  ard-schema                   score=None  mode=notApplicable

The answer to the second opening question: no, it doesn't catch everything. Lighthouse found the button without a name. But it said nothing about the div labeled "Clear", because it doesn't exist as a control in the accessibility tree at all; it can't report a missing name on an element that isn't there. The input named by its placeholder passed too, because the browser gives it a name. In my first attempt, a version that used only a div instead of the icon button passed this audit entirely. The lesson is clear: a green Lighthouse result isn't proof that an agent can use the page; it only proves that the structure you declared to the browser is consistent.

WebMCP and ARD: The Experimental Edges

WebMCP is a proposed standard that lets sites offer structured tools to AI agents through their existing pages. According to the Chrome team's post, trying it requires enabling the chrome://flags/#enable-webmcp-testing flag in Chrome Canary. According to Lighthouse's source code, the WebMCP audits count as "not applicable" if navigator.modelContext doesn't exist in the browser; that's why all three came out notApplicable in our run on stable Chrome 156. The form coverage audit checks whether forms carry WebMCP's declarative tool name and description annotations. We covered the Model Context Protocol itself in the "What Is the Model Context Protocol?" post of the AI Agents and MCP series; WebMCP's browser side is experimental, so I'm not giving code for it here.

The ARD audit is also notApplicable on Bookstore; the site has no ARD manifest. That too is a new and not yet widespread proposal. My advice for both areas is the same: semantic HTML and accessibility work for everyone today; watch WebMCP and ARD, but don't put them into production before they're standardized.

Content in the Initial HTML and a Stable Layout

How agents render a page varies by operator, and there is no official, detailed documentation on it. The safe assumption is the same as in the series' earlier posts: the main content and forms should be in the initial HTML coming from the server. Both pages above were rendered on the server; they carry their accessibility tree even before JavaScript runs. In Angular the way to do that is SSR (Server-Side Rendering) or prerendering; the details are in the Technical SEO (Search Engine Optimization) series, in the posts "JavaScript SEO" and "SEO with Angular SSR".

On CLS, both Bookstore pages scored 0 because they're static and have no images. On real pages, images without dimensions, banners that load late and font swaps shift the layout. Their technical improvement is the subject of the upcoming Web Performance series.

Production Notes

Run agent readiness as part of your accessibility work. Instead of starting a separate "AI compatibility" project, add accessibility tree checks to your existing a11y (accessibility) tests; we'll cover those tests in detail in the upcoming Accessibility series.

Run Lighthouse in CI (Continuous Integration) for critical flows: order tracking, search, add to cart. But use the result as a warning system, not a quality gate; the category describes itself as informational and subject to change.

Try it by hand with a real agent. What Lighthouse can't see, controls built from divs, you find fastest by walking an agent or a screen reader through the page.

Common Pitfalls

Taking a green Lighthouse result to mean "agent friendly". The audit only checks the declared structure; a button built from a div may not show up there at all.

Using a placeholder instead of a label. The browser giving it a name as a fallback is not a lasting solution.

Trying to fix everything with ARIA. <div role="button" tabindex="0"> is possible but requires you to write the keyboard behavior by hand too; the right element is already <button>.

Adopting experimental standards in production too early. WebMCP is behind a flag in stable Chrome today.

A Small Experiment

★☆☆ Open the most important form on your own site and check the role and name of every control in the Accessibility panel of Chrome DevTools. Is there a control without a name or without a role?

★★☆ Add an aria-label to the icon button in the bad version and turn the "Clear" div into a <button type="button">. Take the accessibility tree dump and the Lighthouse result again.

★★★ Call Lighthouse from a test through its Node.js API (Application Programming Interface): if agent-accessibility-tree fails, the test breaks; the other audits are only logged as warnings. Run it only against your local server.

We've talked about how the site looks to bots, answer engines and agents. In the last post of the series we'll try to measure the outcome of all of it: what can Search Console, Bing AI Performance, referral traffic and logs actually tell us?