How to Write Semantic HTML for AI Agents in 2026

Semantic HTML decides which parts of your page an AI extractor keeps and which parts it throws away before the model ever sees them. This page covers the elements that survive HTML-to-markdown conversion, the heading rules that replaced the removed outline algorithm, and three fixes you can ship today.

CategoryStructured data & machine readability StatusStable Maintained byWHATWG / W3C Glippy checkSemantic HTML (category 2)

Semantic HTML for AI agents is the practice of marking content with the elements that say what it is, <main>, <article>, <section>, <nav>, <table>, <figure> and <h1> to <h6>, instead of generic <div> and <span> wrappers. Browsers and assistive technology derive roles and landmarks from those elements, and so do the extractors that turn a page into the text an LLM actually reads. Strip them out and the crawler is left guessing from class names, link density and text length.

Why Semantic HTML matters for AI visibility

Very little AI reading of the web happens on raw HTML. The documented pipelines fetch the page, run a main-content extractor over the DOM, and convert the surviving subtree to markdown before anything reaches a model. Jina's Reader is the clearest published example: headless Chrome for the fetch, Mozilla's Readability for the extraction, Turndown for the markdown. Readability is not a formatter, it is a selector. It scores candidate nodes, picks one subtree as the article, and deletes everything else. Your markup is the input to that decision.

The deletions are specific and worth knowing. Readability removes any element carrying role="navigation" or role="complementary" before it starts scoring, drops wrappers whose class or id matches its unlikely-candidates pattern, then in its clean-up pass deletes every <aside> and <footer> inside the content it just extracted and rewrites every remaining <h1> to <h2>. Turndown then keeps only what has a markdown equivalent: headings, lists, links, blockquotes, code blocks, emphasis. Tables and definition lists have no core rule, so their cells fall through to the default block handler and come out as loose paragraphs. On top of that, the major AI crawlers fetch raw HTML and do not execute JavaScript, so structure you add on the client is structure the extractor never sees.

Where the spec lives

Semantic HTML is stable, but the guidance around headings moved in the last few years. Read the WHATWG heading rules and the ARIA role mapping first, then the extractor source, because that is where the real behaviour is.

  • WHATWG HTML Standard, 4.3.11 Headings and outlines - the normative rule: each heading must be less than, equal to, or one greater than the previous heading in tree order. Includes the non-conforming h1-then-h3 example.
  • W3C ARIA in HTML - the implicit role table. Check it before you assume an element is a landmark: a <section> without an accessible name is only role=generic.
  • ARIA APG, Landmark Regions - how many banner, main and contentinfo landmarks a page may have, and when a landmark needs its own label.
  • MDN, the h1 to h6 elements - current heading practice, including the note that nesting h1 elements inside sectioning elements is now non-conforming.
  • mozilla/readability - the extraction code most reader and AI pipelines run. Read _grabArticle, _prepArticle and _markDataTables to see exactly what gets dropped.
  • mixmark-io/turndown - the HTML-to-markdown converter. The core rule set covers paragraphs, headings, lists, links, code and emphasis, and nothing else.

Three ways to implement Semantic HTML

Three problems, in the order they usually bite. First, a page built entirely from divs, where the extractor has nothing to anchor on: fix that with landmarks. Second, a component-composed page whose heading levels are broken because each component brings its own <h1>: fix that with explicit levels, because the outline algorithm that used to rescue you is gone. Third, the content that survives extraction badly, tables and definitions: fix that with the markup that extractors specifically look for.

01

Restructure a div-soup page into landmarks

Use this when your page is a stack of <div class="..."> and the only structural signal an extractor has is your class naming. The layout and the CSS do not need to change: you are swapping the tag names, keeping the classes you still need for styling, and adding the labels that turn a generic box into a named region.

htmltemplates/base.html
<!-- Before: the extractor has to guess from class names -->
<div class="page-header">...</div>
<div class="sidebar">...</div>
<div class="content">...</div>

<!-- After: same layout, elements that carry the roles -->
<body>
  <header>
    <a class="logo" href="/">Northwind Tools</a>
    <nav aria-label="Primary">
      <ul><li><a href="/docs/">Docs</a></li></ul>
    </nav>
    <search>
      <form action="/search">
        <label for="q">Search</label>
        <input id="q" name="q" type="search">
      </form>
    </search>
  </header>

  <main>
    <article>
      <h1>Torque wrench calibration intervals</h1>
      <p>Updated <time datetime="2026-06-14">14 June 2026</time>.</p>
      <p>Calibrate every 5,000 cycles or 12 months, whichever comes first.</p>
      <section aria-labelledby="method">
        <h2 id="method">Calibration method</h2>
        <p>Compare the tool against a reference transducer at 20, 60 and 100 percent of range.</p>
      </section>
    </article>
  </main>

  <aside aria-label="Related guides">
    <h2>Related guides</h2>
    <ul><li><a href="/docs/torque-specs/">Torque specifications</a></li></ul>
  </aside>

  <footer>
    <address>Northwind Tools, 12 Bridge Street, Leeds LS1 4AP</address>
  </footer>
</body>

What this does: the extractor now has an unambiguous target. Chrome and every screen reader expose a single main landmark, a navigation landmark, a complementary landmark and a contentinfo landmark, and Readability can drop the header, nav, aside and footer as a unit instead of scoring your class names. Note that <header> and <footer> only map to the banner and contentinfo landmarks when they are not descendants of article, aside, main, nav or section, which is why they sit outside <main> here.

02

Fix a broken heading hierarchy on a component-composed page

Use this when the page is assembled from components that each hardcode their own heading tag. This is the single most common cause of a page with four <h1> elements and an <h1> to <h3> jump. The old advice, that nesting <h1> inside a <section> makes it behave as an <h2>, has not been true since the outline algorithm was removed from the spec in 2022, and browsers finished dropping the matching default styles through 2025.

htmltemplates/pricing.html
<!-- Before: three h1 elements and an h1 to h3 skip -->
<main>
  <h1>Pricing</h1>
  <section>
    <h1>Team plan</h1>
    <h3>What is included</h3>
  </section>
  <section>
    <h1>Enterprise plan</h1>
  </section>
</main>

<!-- After: one h1, single-step levels, each section named by its own heading -->
<main>
  <h1>Pricing</h1>
  <section aria-labelledby="team-plan">
    <h2 id="team-plan">Team plan</h2>
    <h3>What is included</h3>
    <ul>
      <li>Up to 25 seats</li>
      <li>Shared audit history</li>
    </ul>
  </section>
  <section aria-labelledby="enterprise-plan">
    <h2 id="enterprise-plan">Enterprise plan</h2>
    <h3>What is included</h3>
    <ul>
      <li>Unlimited seats</li>
      <li>SSO and SCIM</li>
    </ul>
  </section>
</main>

<!-- Not yet: headingoffset is Firefox Nightly only, behind dom.headingoffset.enabled -->
<!-- <div headingoffset="1"><h2>Team plan</h2></div> -->

What this does: the markdown an extractor produces now has one # heading and a clean ## and ### tree under it, which is the only outline a model gets. The durable fix in a component system is to pass the heading level in as a prop and render h{level} on the server, so a card that sits at the top of a page emits an h2 and the same card nested in a panel emits an h3. The headingoffset attribute is designed to solve exactly this, but as of August 2026 it ships only in Firefox Nightly behind a flag, so it is not a production answer yet.

03

Mark up a data table and a definition so they survive text extraction

Use this for the content that gets quoted back: specifications, pricing tiers, limits, and the one-line definition of your own term. These are exactly the blocks that arrive at the model as scrambled paragraphs when the markup is thin, because a bare <table> of <tr> and <td> gives an extractor no way to tell a data table from a layout table.

htmltemplates/calibration.html
<section aria-labelledby="intervals">
  <h2 id="intervals">Calibration intervals by tool class</h2>

  <p><strong><dfn id="calibration-interval">Calibration interval</dfn></strong>
     is the maximum time or cycle count between two verifications of a torque
     tool against a reference standard.</p>

  <table>
    <caption>Calibration intervals by tool class, effective June 2026</caption>
    <thead>
      <tr>
        <th scope="col">Tool class</th>
        <th scope="col">Cycle limit</th>
        <th scope="col">Time limit</th>
        <th scope="col">Standard</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">Click wrench</th>
        <td>5,000 cycles</td>
        <td>12 months</td>
        <td>ISO 6789-2</td>
      </tr>
      <tr>
        <th scope="row">Digital wrench</th>
        <td>2,500 cycles</td>
        <td>6 months</td>
        <td>ISO 6789-2</td>
      </tr>
    </tbody>
  </table>

  <figure>
    <img src="/img/torque-drift.svg" alt="Torque drift against cycle count, rising sharply past 5,000 cycles">
    <figcaption>Measured drift for a click wrench, sampled every 500 cycles.</figcaption>
  </figure>
</section>

What this does: the <caption>, <thead> and <th> cells are the exact signals Readability uses to mark a table as a data table, which exempts it from the conditional clean-up that strips layout tables. The <th scope="row"> in the first column keeps the row label attached to its values even when a converter flattens the cells into separate paragraphs. Wrapping the term in <strong> matters as much as the <dfn>: <dfn> has no markdown equivalent and comes out as plain text, whereas <strong> survives the conversion and keeps the term visibly marked in the extracted text.

Implementation guidelines

Seven things that decide whether your structure reaches the model intact.

  1. One h1, then never skip a level. The spec requires each heading to have a level less than, equal to, or one greater than the previous heading in document order, so an <h1> followed by an <h3> is non-conforming, not merely untidy. Multiple <h1> elements are technically allowed but are no longer scoped by anything, so treat one per page as the rule.
  2. Put the unique content in a single main. Everything that is the same across pages, the site header, primary navigation and footer, belongs outside it. <header> and <footer> only map to the banner and contentinfo landmarks when they are not descendants of article, aside, main, nav or section.
  3. Name every section, or use article instead. Per ARIA in HTML a <section> maps to role=region only when it has an accessible name, and to role=generic otherwise. Point aria-labelledby at the id of the section's own heading, which costs one attribute and one id.
  4. Do not park quotable content in aside or footer. Readability's article preparation step calls its clean routine on both tags and deletes every instance inside the subtree it extracted, with no class-name reprieve. Pricing, specifications, contact details and summaries belong in <main>.
  5. Audit class and id names, not just tag names. Readability drops wrappers whose class or id contains strings such as header, footer, sidebar, menu, related, banner, breadcrumbs, social, comment or pagination, unless the same string also matches article, body, column, content or main. It also removes any h1 or h2 whose class weight is negative, so a heading marked class="related-heading" disappears from the extracted text.
  6. Render the structure on the server. The major AI crawlers fetch raw HTML and do not execute JavaScript, so landmarks and headings injected on the client never reach the extractor. Server rendering also protects your content-to-markup ratio, because hydration payloads and long utility-class strings are markup and count against you.
  7. Put a heading rule in CI. The heading-level rule in html-validate enforces "start at h1 and increase one level at a time" and rejects multiple <h1> elements by default, so a component that emits its own top-level heading fails the build rather than the crawl.

Do this, not that

Do

  • Give the page exactly one <h1>, then move down in single steps to <h2> and <h3>.
  • Wrap the page's unique content in one <main>, with <header>, <nav> and <footer> as siblings of it.
  • Give each <section> an aria-labelledby pointing at its own heading id so it becomes a named region.
  • Give data tables a <caption>, a <thead>, and <th scope="col"> plus <th scope="row"> cells.

Do not

  • Do not nest an <h1> inside a <section> expecting it to count as an <h2>. The outline algorithm was removed in 2022 and the browser styles that masked its absence went in 2025.
  • Do not put text you want quoted inside <aside> or <footer>, and do not name a content wrapper sidebar, related or promo.
  • Do not set role="navigation" or role="complementary" on a wrapper that holds body content: Readability removes nodes with those roles before it scores anything.
  • Do not ship headingoffset as the fix for nested component headings. As of August 2026 it exists only in Firefox Nightly behind the dom.headingoffset.enabled flag.

How Glippy checks this

Semantic HTML is category 2 in Glippy, weighted at 1.2, and it scores six signals out of 100. It counts <h1> elements and passes only on exactly one, walks the full h1 to h6 sequence and warns if any level jumps by more than one, and looks for distinct semantic element types among main, article, section, nav, aside, footer, figure, time and address, passing at five or more. It then measures the content-to-markup ratio, the extracted body text length divided by the full HTML length, passing above 15 percent and warning between 8 and 15, plus word count with a 300-word pass threshold and a Flesch-Kincaid readability band of roughly 30 to 70. The Semantic HTML Checker shows the per-signal breakdown for any URL.

Check your Semantic HTML setup

Glippy runs 240+ checks across 16 categories on any page, including Semantic HTML (category 2). No sign-up required.

Frequently asked questions

One. The outline algorithm that used to scope an h1 inside a section down to an h2 was removed from the HTML spec in 2022, and browsers finished removing the matching default styles during 2025, so a second h1 is now simply a second top-level heading. Mozilla's Readability, which sits inside most reader and AI extraction pipelines, rewrites every h1 found inside the extracted article to h2 because it treats the page title as separate. Glippy scores a single h1 at full marks and warns when it finds more than one.

They read the elements, but indirectly. The documented pipelines fetch the HTML, run a main-content extractor such as Mozilla's Readability to pick one subtree, then convert that subtree to markdown with a library such as Turndown. Headings, lists, links, blockquotes, code blocks and emphasis have markdown equivalents and survive the conversion; navigation, complementary and footer content is usually deleted before the conversion runs; and tables need an explicit table plugin or their cells flatten into separate paragraphs.

No. Mozilla's Readability deletes every aside and footer inside the content it extracted, and it removes elements carrying role="complementary" or role="navigation" even earlier, before any scoring happens. Anything you want an assistant to quote back, such as pricing, specifications, contact details or a summary, belongs inside main. Reserve aside for content that is genuinely tangential and that you would not mind losing.

Glippy passes a page above 15 percent and warns between 8 and 15 percent, comparing the extracted body text length against the full HTML length in characters. Long utility-class strings, inline SVG sprites, repeated layout wrappers and serialised hydration state all sit in the denominator. The practical fixes are moving icon sprites into an external file, deleting wrapper divs that exist only for layout, and keeping serialised application state out of the document.

Reviewed against the primary sources on . These standards move quickly, so check the linked specs before you ship.

Check your site for AI search readiness

Start free with the Glippy Chrome extension for instant page checks. Scaling up? Automate audits across many URLs and your whole sitemap with the Glippy MCP server.

Add Glippy to Chrome – free Automate with the MCP server →