references/checkpoints.md
<!-- Generated by scripts/sync-checkpoints.mjs. Do not edit by hand. -->
# Checkpoint reference
This file is generated by `scripts/sync-checkpoints.mjs`. Do not edit it by hand. Run `node scripts/sync-checkpoints.mjs` to update it.
- Skill: `content-semantics`
- Methodology version: `2026-10-30`
- Methodology: https://isagentready.com/en/methodology
- Scoring: https://isagentready.com/api/v2/methodology/2026-10-30/scoring
- Replace `{domain}` with the domain of the site. Each verify command is read-only.
- Strength and evidence type are spec values. They do not change the score.
- Scanner data comes from the latest published benchmark. A snapshot has no scanner data.
## Checkpoints
| Legacy | Stable ID | Name | Stage | Strength | AR IDs | Evidence |
|---|---|---|---|---|---|---|
| 3.1 | [`ia.server-rendered-content`](#ia.server-rendered-content) | Server-side rendered content | Read | MUST | AR-READ-01, AR-READ-03 | measured_study |
| 3.2 | [`ia.heading-hierarchy`](#ia.heading-hierarchy) | Heading hierarchy | Read | MUST | AR-READ-03 | recommendation |
| 3.3 | [`ia.semantic-html`](#ia.semantic-html) | Semantic HTML elements | Read | MUST | none | recommendation |
| 3.4 | [`ia.landmarks-aria`](#ia.landmarks-aria) | Landmarks and ARIA | Read | SHOULD | none | recommendation |
| 3.5 | [`ia.image-alt`](#ia.image-alt) | Image alt text | Read | MUST | none | recommendation |
| 3.6 | [`ia.lang-attribute`](#ia.lang-attribute) | Language attribute | Read | MUST | none | recommendation |
| 3.7 | [`ia.link-text`](#ia.link-text) | Descriptive link texts | Read | MUST | none | recommendation |
| 3.9 | [`ia.form-labels`](#ia.form-labels) | Form label coverage | Act | SHOULD | none | recommendation |
| 3.10 | [`ia.form-autocomplete`](#ia.form-autocomplete) | Form field autocomplete | Act | SHOULD | none | recommendation |
| 3.11 | [`ia.keyboard-navigation`](#ia.keyboard-navigation) | Keyboard navigability | Act | SHOULD | none | recommendation |
| 3.12 | [`ia.document-title`](#ia.document-title) | Document title | Read | MUST | AR-READ-03 | recommendation |
| 3.13 | [`ia.button-names`](#ia.button-names) | Button accessible names | Act | SHOULD | none | recommendation |
| 3.14 | [`ia.aria-hidden`](#ia.aria-hidden) | aria-hidden misuse | Read | SHOULD | none | recommendation |
| 3.15 | [`ia.widget-state`](#ia.widget-state) | Dynamic widget state | Act | SHOULD | none | recommendation |
## Details
<a id="ia.server-rendered-content"></a>
### 3.1 Server-side rendered content
- Stable ID: `ia.server-rendered-content`
- Stage: Read
- Strength: MUST
- AR practices: AR-READ-01, AR-READ-03 (mapping: full)
- Evidence type: `measured_study`
- Fix: Render the main content in the HTML response, without JavaScript.
- Fix workflow: [SKILL.md](../SKILL.md#ia.server-rendered-content)
- Evidence source: [agentready.org AR-READ-01](https://www.agentready.org/#read)
- Applicability: always (website)
- Pass condition: Meaningful text content in the initial HTML response
- Expected result: The HTML response contains the main headings and body text
- Rescan: `isagentready results {domain} --checkpoint ia.server-rendered-content`
- Details: https://isagentready.com/en/methodology/checkpoints/ia.server-rendered-content
Why: Fetch-only crawlers and non-rendered retrieval cannot depend on client-side JavaScript. Browser agents may combine the rendered DOM, accessibility tree, and screenshots. Meaningful server-rendered HTML keeps core content available across both access modes.
References:
- [Build agent-friendly websites](https://web.dev/articles/ai-agent-site-ux) (guide)
- [Google JavaScript SEO basics](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics) (guide)
Steps:
1. Fetch the page without JavaScript and compare the text with the rendered page.
2. Enable server-side rendering or static generation for the main content.
3. Keep headings, body text and links in the initial HTML.
Verify:
```bash
curl -sf https://{domain}/
```
<a id="ia.heading-hierarchy"></a>
### 3.2 Heading hierarchy
- Stable ID: `ia.heading-hierarchy`
- Stage: Read
- Strength: MUST
- AR practices: AR-READ-03 (mapping: partial)
- Evidence type: `recommendation`
- Fix: Use one h1 and do not skip heading levels.
- Fix workflow: [SKILL.md](../SKILL.md#ia.heading-hierarchy)
- Evidence source: [agentready.org AR-READ-03](https://www.agentready.org/#read)
- Applicability: always (website)
- Rescan: `isagentready results {domain} --checkpoint ia.heading-hierarchy`
- Details: https://isagentready.com/en/methodology/checkpoints/ia.heading-hierarchy
Why: AI agents use headings to build a content outline and determine topic hierarchy. A clear h1→h2→h3 structure helps AI systems extract and summarize your content accurately.
References:
- [MDN Heading Elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Heading_Elements) (guide)
- [W3C Using h1-h6 to identify headings](https://www.w3.org/WAI/WCAG22/Techniques/html/H42) (spec)
Steps:
1. Give each page one h1 that names the main topic.
2. Use h2 for sections and h3 for subsections, without skipped levels.
```html
<h1>Pricing</h1>
<h2>Plans</h2>
<h3>Team plan</h3>
```
Verify:
No read-only shell check exists. Use the rescan command.
<a id="ia.semantic-html"></a>
### 3.3 Semantic HTML elements
- Stable ID: `ia.semantic-html`
- Stage: Read
- Strength: MUST
- AR practices: none (mapping: none)
- Evidence type: `recommendation`
- Fix: Use main, nav, header, footer and article for the page structure.
- Fix workflow: [SKILL.md](../SKILL.md#ia.semantic-html)
- Evidence source: [MDN Semantics in HTML](https://developer.mozilla.org/en-US/docs/Glossary/Semantics#semantics_in_html)
- Applicability: always (website)
- Rescan: `isagentready results {domain} --checkpoint ia.semantic-html`
- Details: https://isagentready.com/en/methodology/checkpoints/ia.semantic-html
Why: Browser agents may combine the DOM, accessibility tree, and screenshots. Semantic elements like <main>, <nav>, and <article> provide durable structural meaning across those representations.
References:
- [MDN Semantics in HTML](https://developer.mozilla.org/en-US/docs/Glossary/Semantics#semantics_in_html) (guide)
- [W3C HTML5 Document Sections](https://www.w3.org/TR/html52/sections.html) (spec)
Steps:
1. Replace layout div elements with semantic elements.
```html
<header>...</header>
<nav>...</nav>
<main>
<article>...</article>
</main>
<footer>...</footer>
```
2. Use one main element per page.
Verify:
No read-only shell check exists. Use the rescan command.
<a id="ia.landmarks-aria"></a>
### 3.4 Landmarks and ARIA
- Stable ID: `ia.landmarks-aria`
- Stage: Read
- Strength: SHOULD
- AR practices: none (mapping: none)
- Evidence type: `recommendation`
- Fix: Give each page region a landmark and label repeated landmarks.
- Fix workflow: [SKILL.md](../SKILL.md#ia.landmarks-aria)
- Evidence source: [W3C Landmarks Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/)
- Applicability: always (website)
- Rescan: `isagentready results {domain} --checkpoint ia.landmarks-aria`
- Details: https://isagentready.com/en/methodology/checkpoints/ia.landmarks-aria
Why: ARIA landmarks help AI agents identify page regions (navigation, main content, footer). This is the same API used by screen readers and is increasingly used by AI browsing agents.
References:
- [W3C Landmarks Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/) (guide)
- [MDN ARIA Roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles) (guide)
Steps:
1. Make sure that each page has a main, a navigation and a contentinfo landmark.
2. Add aria-label when a landmark type occurs more than once.
```html
<nav aria-label="Main">...</nav>
<nav aria-label="Footer">...</nav>
```
Verify:
No read-only shell check exists. Use the rescan command.
<a id="ia.image-alt"></a>
### 3.5 Image alt text
- Stable ID: `ia.image-alt`
- Stage: Read
- Strength: MUST
- AR practices: none (mapping: none)
- Evidence type: `recommendation`
- Fix: Give each content image a descriptive alt text and decorative images an empty alt.
- Fix workflow: [SKILL.md](../SKILL.md#ia.image-alt)
- Evidence source: [WCAG Non-text Content](https://www.w3.org/WAI/WCAG22/Understanding/non-text-content.html)
- Applicability: always (website)
- Rescan: `isagentready results {domain} --checkpoint ia.image-alt`
- Details: https://isagentready.com/en/methodology/checkpoints/ia.image-alt
Why: Some browser agents can inspect screenshots, but image understanding varies by access mode. Descriptive alt text provides a durable machine-readable description of image content and context.
References:
- [WCAG Non-text Content](https://www.w3.org/WAI/WCAG22/Understanding/non-text-content.html) (spec)
- [MDN img alt attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#alt) (guide)
Steps:
1. Add an alt attribute that describes the content of each informative image.
```html
<img src="chart.png" alt="Revenue grew 20% in 2026">
```
2. Use alt="" for decorative images.
Verify:
No read-only shell check exists. Use the rescan command.
<a id="ia.lang-attribute"></a>
### 3.6 Language attribute
- Stable ID: `ia.lang-attribute`
- Stage: Read
- Strength: MUST
- AR practices: none (mapping: none)
- Evidence type: `recommendation`
- Fix: Add a valid lang attribute to the html element.
- Fix workflow: [SKILL.md](../SKILL.md#ia.lang-attribute)
- Evidence source: [MDN lang attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang)
- Applicability: always (website)
- Expected result: The html element has a lang attribute
- Rescan: `isagentready results {domain} --checkpoint ia.lang-attribute`
- Details: https://isagentready.com/en/methodology/checkpoints/ia.lang-attribute
Why: The lang attribute tells AI agents what language your content is in, enabling correct text processing, translation, and language-specific understanding.
References:
- [MDN lang attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang) (guide)
- [W3C Declaring language in HTML](https://www.w3.org/International/questions/qa-html-language-declarations) (guide)
Steps:
1. Set the lang attribute on the html element to the language of the page.
```html
<html lang="en">
```
Verify:
```bash
curl -sf https://{domain}/
```
<a id="ia.link-text"></a>
### 3.7 Descriptive link texts
- Stable ID: `ia.link-text`
- Stage: Read
- Strength: MUST
- AR practices: none (mapping: none)
- Evidence type: `recommendation`
- Fix: Replace generic link texts with texts that name the target.
- Fix workflow: [SKILL.md](../SKILL.md#ia.link-text)
- Evidence source: [WCAG Link Purpose (In Context)](https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html)
- Applicability: always (website)
- Rescan: `isagentready results {domain} --checkpoint ia.link-text`
- Details: https://isagentready.com/en/methodology/checkpoints/ia.link-text
Why: AI agents use link text to understand navigation and discover related content. Generic text like 'click here' provides no context about where the link leads.
References:
- [WCAG Link Purpose (In Context)](https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html) (spec)
- [MDN Anchor element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a) (guide)
Steps:
1. Find links with texts such as click here, read more or here.
2. Change the text so that it names the target page.
```html
<a href="/pricing">See the pricing plans</a>
```
Verify:
No read-only shell check exists. Use the rescan command.
<a id="ia.form-labels"></a>
### 3.9 Form label coverage
- Stable ID: `ia.form-labels`
- Stage: Act
- Strength: SHOULD
- AR practices: none (mapping: none)
- Evidence type: `recommendation`
- Fix: Give each form field a visible label that is linked with for and id.
- Fix workflow: [SKILL.md](../SKILL.md#ia.form-labels)
- Evidence source: [WCAG Labels or Instructions](https://www.w3.org/WAI/WCAG22/Understanding/labels-or-instructions.html)
- Applicability: always (website)
- Rescan: `isagentready results {domain} --checkpoint ia.form-labels`
- Details: https://isagentready.com/en/methodology/checkpoints/ia.form-labels
Why: AI agents interact with forms via the accessibility tree, which maps labels to inputs. Without proper label associations, AI agents cannot determine what data a form field expects, making form interaction unreliable or impossible.
References:
- [WCAG Labels or Instructions](https://www.w3.org/WAI/WCAG22/Understanding/labels-or-instructions.html) (spec)
- [MDN label element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label) (guide)
Steps:
1. Add a label element for each input, select and textarea.
```html
<label for="email">Email</label>
<input id="email" name="email" type="email">
```
2. Use aria-label only when a visible label is not possible.
Verify:
No read-only shell check exists. Use the rescan command.
<a id="ia.form-autocomplete"></a>
### 3.10 Form field autocomplete
- Stable ID: `ia.form-autocomplete`
- Stage: Act
- Strength: SHOULD
- AR practices: none (mapping: none)
- Evidence type: `recommendation`
- Fix: Add autocomplete tokens to personal data fields.
- Fix workflow: [SKILL.md](../SKILL.md#ia.form-autocomplete)
- Evidence source: [WHATWG Autofill](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill)
- Applicability: always (website)
- Rescan: `isagentready results {domain} --checkpoint ia.form-autocomplete`
- Details: https://isagentready.com/en/methodology/checkpoints/ia.form-autocomplete
Why: The autocomplete attribute tells AI agents and browsers what type of data a field expects (email, name, address). This semantic hint helps AI agents fill forms accurately without guessing field purposes from visual context.
References:
- [WHATWG Autofill](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill) (spec)
- [WCAG Identify Input Purpose](https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html) (spec)
Steps:
1. Add the matching WHATWG autocomplete token to each personal data field.
```html
<input name="email" type="email" autocomplete="email">
<input name="name" autocomplete="name">
```
Verify:
No read-only shell check exists. Use the rescan command.
<a id="ia.keyboard-navigation"></a>
### 3.11 Keyboard navigability
- Stable ID: `ia.keyboard-navigation`
- Stage: Act
- Strength: SHOULD
- AR practices: none (mapping: none)
- Evidence type: `recommendation`
- Fix: Remove positive tabindex values and make custom controls focusable.
- Fix workflow: [SKILL.md](../SKILL.md#ia.keyboard-navigation)
- Evidence source: [MDN tabindex](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex)
- Applicability: always (website)
- Rescan: `isagentready results {domain} --checkpoint ia.keyboard-navigation`
- Details: https://isagentready.com/en/methodology/checkpoints/ia.keyboard-navigation
Why: AI agents navigate pages using keyboard-style interactions via the accessibility tree. Positive tabindex values break natural focus order, and missing focusable elements on interactive widgets prevent AI agents from discovering and operating controls.
References:
- [MDN tabindex](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) (guide)
- [W3C Focus Order](https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html) (spec)
Steps:
1. Replace tabindex values above 0 with 0 or remove them.
2. Use native button and a elements, or add tabindex="0" and a role to custom controls.
```html
<div role="button" tabindex="0">Open menu</div>
```
Verify:
No read-only shell check exists. Use the rescan command.
<a id="ia.document-title"></a>
### 3.12 Document title
- Stable ID: `ia.document-title`
- Stage: Read
- Strength: MUST
- AR practices: AR-READ-03 (mapping: partial)
- Evidence type: `recommendation`
- Fix: Give each page a unique, descriptive title element.
- Fix workflow: [SKILL.md](../SKILL.md#ia.document-title)
- Evidence source: [agentready.org AR-READ-03](https://www.agentready.org/#read)
- Applicability: always (website)
- Expected result: The head contains a non-empty title element
- Rescan: `isagentready results {domain} --checkpoint ia.document-title`
- Details: https://isagentready.com/en/methodology/checkpoints/ia.document-title
Why: AI agents use the <title> element as the primary label when citing, indexing, or summarizing a page. A missing, empty, or generic title makes your page unidentifiable in AI-generated responses and search results.
References:
- [MDN title element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title) (guide)
- [WCAG Page Titled](https://www.w3.org/WAI/WCAG22/Understanding/page-titled.html) (spec)
Steps:
1. Add a title element in the head with the page topic and the site name.
```html
<title>Pricing - Example</title>
```
Verify:
```bash
curl -sf https://{domain}/
```
<a id="ia.button-names"></a>
### 3.13 Button accessible names
- Stable ID: `ia.button-names`
- Stage: Act
- Strength: SHOULD
- AR practices: none (mapping: none)
- Evidence type: `recommendation`
- Fix: Give each button a text or an aria-label.
- Fix workflow: [SKILL.md](../SKILL.md#ia.button-names)
- Evidence source: [WCAG Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html)
- Applicability: always (website)
- Rescan: `isagentready results {domain} --checkpoint ia.button-names`
- Details: https://isagentready.com/en/methodology/checkpoints/ia.button-names
Why: AI agents invoke buttons by their accessible name in the accessibility tree. Without a discernible name, an AI agent cannot identify what a button does, making form submission and page interaction impossible.
References:
- [WCAG Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) (spec)
- [MDN button element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button) (guide)
Steps:
1. Add visible text to each button, or an aria-label for icon buttons.
```html
<button type="button" aria-label="Close dialog"><svg aria-hidden="true">...</svg></button>
```
Verify:
No read-only shell check exists. Use the rescan command.
<a id="ia.aria-hidden"></a>
### 3.14 aria-hidden misuse
- Stable ID: `ia.aria-hidden`
- Stage: Read
- Strength: SHOULD
- AR practices: none (mapping: none)
- Evidence type: `recommendation`
- Fix: Remove aria-hidden from the body and from focusable elements.
- Fix workflow: [SKILL.md](../SKILL.md#ia.aria-hidden)
- Evidence source: [MDN aria-hidden](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-hidden)
- Applicability: always (website)
- Rescan: `isagentready results {domain} --checkpoint ia.aria-hidden`
- Details: https://isagentready.com/en/methodology/checkpoints/ia.aria-hidden
Why: aria-hidden="true" removes elements from the accessibility tree. When applied to the <body> or to focusable elements, it corrupts the tree that AI agents navigate, causing them to miss content or encounter invisible controls.
References:
- [MDN aria-hidden](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-hidden) (guide)
- [WCAG Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) (spec)
Steps:
1. Remove aria-hidden="true" from the body element.
2. Remove aria-hidden from links, buttons and inputs, or make them not focusable.
Verify:
No read-only shell check exists. Use the rescan command.
<a id="ia.widget-state"></a>
### 3.15 Dynamic widget state
- Stable ID: `ia.widget-state`
- Stage: Act
- Strength: SHOULD
- AR practices: none (mapping: none)
- Evidence type: `recommendation`
- Fix: Expose the state of interactive widgets with ARIA state attributes.
- Fix workflow: [SKILL.md](../SKILL.md#ia.widget-state)
- Evidence source: [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
- Applicability: always (website)
- Rescan: `isagentready results {domain} --checkpoint ia.widget-state`
- Details: https://isagentready.com/en/methodology/checkpoints/ia.widget-state
Why: Agents operate interactive widgets through the accessibility tree. Expandable panels, tabs, menus, switches, dialogs, and async status areas need explicit state so agents can tell what is open, selected, checked, current, or updating.
References:
- [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/) (guide)
- [MDN ARIA states and properties](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes) (guide)
- [WCAG Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) (spec)
Steps:
1. Add aria-expanded and aria-controls to disclosure buttons.
```html
<button aria-expanded="false" aria-controls="filters">Filters</button>
<section id="filters" hidden>...</section>
```
2. Add aria-selected to tabs, aria-checked to switches and role="status" to async messages.
Verify:
No read-only shell check exists. Use the rescan command.
references/gotchas.md
# Content & Semantics Gotchas
Common pitfalls when implementing semantic HTML for AI agent readiness. Each gotcha includes WRONG and CORRECT examples.
---
## 1. Multiple `<h1>` Tags from Component Composition
Components that include their own `<h1>` create duplicate headings when composed into a page.
**WRONG:**
```tsx
// Header component
function Header() {
return <header><h1>Company Name</h1></header>;
}
// Page component: now the page has TWO <h1> tags
function ProductPage() {
return (
<>
<Header />
<main>
<h1>Our Products</h1> {/* Second h1! */}
</main>
</>
);
}
```
**CORRECT:**
```tsx
// Header component uses logo/link, not h1
function Header() {
return (
<header>
<a href="/" aria-label="Home">
<img src="logo.svg" alt="Company Name">
</a>
</header>
);
}
// Each page defines its own single h1
function ProductPage() {
return (
<>
<Header />
<main>
<h1>Our Products</h1>
</main>
</>
);
}
```
**Rule:** The `<h1>` is always the page title, never a site-wide branding element.
---
## 2. Heading Level Resets in Reusable Components
Components that hardcode heading levels break hierarchy when used at different nesting depths.
**WRONG:**
```tsx
// Card component always uses <h3>
function FeatureCard({ title, description }) {
return (
<div>
<h3>{title}</h3> {/* What if parent context is <h2>? Or <h4>? */}
<p>{description}</p>
</div>
);
}
// Used under <h2>: fine
<h2>Features</h2>
<FeatureCard title="Speed" /> {/* h3 is correct here */}
// Used under <h4>: broken! h3 after h4 is a level skip backwards
<h4>Advanced Features</h4>
<FeatureCard title="Custom API" /> {/* h3 is wrong here */}
```
**CORRECT:**
```tsx
// Accept heading level as a prop
function FeatureCard({ title, description, headingLevel = 'h3' }) {
const Heading = headingLevel;
return (
<div>
<Heading>{title}</Heading>
<p>{description}</p>
</div>
);
}
// Use at appropriate level
<h2>Features</h2>
<FeatureCard title="Speed" headingLevel="h3" />
<h4>Advanced Features</h4>
<FeatureCard title="Custom API" headingLevel="h5" />
```
---
## 3. `<noscript>` Is Not a Substitute for SSR
Adding `<noscript>` provides a fallback but does NOT make your content visible to AI agents that parse HTML normally (they don't execute JS but they also don't specifically look for `<noscript>`).
**WRONG: Thinking noscript solves the problem:**
```html
<body>
<div id="root"></div>
<noscript>
<p>Please enable JavaScript</p>
</noscript>
<script src="/bundle.js"></script>
</body>
<!-- AI agents see: empty root div + "Please enable JavaScript" -->
<!-- Result: partial, not pass -->
```
**CORRECT: Server-render actual content:**
```html
<body>
<main>
<h1>Our Products</h1>
<article>
<h2>Widget Pro</h2>
<p>The fastest widget on the market...</p>
</article>
</main>
<script src="/bundle.js"></script>
</body>
<!-- AI agents see: full content in HTML -->
<!-- Result: pass -->
```
**Rule:** `<noscript>` is partial credit. Real SSR is the only way to get full score.
---
## 4. `<article>` vs `<section>` Confusion
Using `<article>` and `<section>` interchangeably. They have different semantic meanings.
**WRONG:**
```html
<!-- Using article for non-standalone content -->
<article>
<h2>Pricing</h2>
<p>Choose your plan</p>
</article>
<!-- Using section for standalone content -->
<section>
<h2>How AI Agents Parse HTML</h2>
<p>AI agents use the accessibility tree...</p>
<p>Published: Feb 27, 2026 by Jane Doe</p>
</section>
```
**CORRECT:**
```html
<!-- Section: thematic grouping within a page -->
<section>
<h2>Pricing</h2>
<p>Choose your plan</p>
</section>
<!-- Article: standalone, redistributable content -->
<article>
<h2>How AI Agents Parse HTML</h2>
<p>AI agents use the accessibility tree...</p>
<footer>Published: Feb 27, 2026 by Jane Doe</footer>
</article>
```
**Test:** Would this make sense in an RSS feed or if shared independently? → `<article>`. Is it just a thematic grouping within a page? → `<section>`.
---
## 5. Redundant ARIA Roles on Semantic Elements
Adding explicit `role` attributes that duplicate the implicit role of semantic elements. This is warned against by the W3C.
**WRONG:**
```html
<header role="banner">
<nav role="navigation">
<main role="main">
<footer role="contentinfo">
<aside role="complementary">
```
**CORRECT:**
```html
<!-- Semantic elements provide implicit roles: no role attribute needed -->
<header>
<nav>
<main>
<footer>
<aside>
<!-- Only use explicit roles on non-semantic elements when refactoring isn't possible -->
<div role="navigation"> <!-- Only when you can't change the div to nav -->
```
**When to use explicit `role`:** Only when you cannot change the underlying element (e.g., third-party component that renders `<div>` but acts as navigation).
---
## 6. Generic Link Text Hidden in Buttons and Components
Generic link text inside styled buttons or components that look descriptive visually but are generic in the accessibility tree.
**WRONG:**
```html
<!-- Looks like a button, but link text is "Read more" -->
<a href="/blog/ai-trends" class="btn btn-primary">
Read more
</a>
<!-- Card where only "Learn more" is the link -->
<div class="card">
<h3>AI Trends 2026</h3>
<p>The latest developments in AI agent technology...</p>
<a href="/blog/ai-trends">Learn more</a>
</div>
<!-- Icon-only link with no accessible text -->
<a href="/settings">
<svg>...</svg>
</a>
```
**CORRECT:**
```html
<!-- Descriptive visible text -->
<a href="/blog/ai-trends" class="btn btn-primary">
Read AI Trends 2026
</a>
<!-- Option A: Make entire card a link with descriptive heading -->
<a href="/blog/ai-trends" class="card">
<h3>AI Trends 2026</h3>
<p>The latest developments in AI agent technology...</p>
</a>
<!-- Option B: Use aria-label when visual design requires short text -->
<div class="card">
<h3>AI Trends 2026</h3>
<p>The latest developments in AI agent technology...</p>
<a href="/blog/ai-trends" aria-label="Read AI Trends 2026">Learn more</a>
</div>
<!-- Icon link with aria-label -->
<a href="/settings" aria-label="Account settings">
<svg aria-hidden="true">...</svg>
</a>
```
---
## 7. Decorative Images: Missing `alt` vs Empty `alt`
There's a critical difference between a missing `alt` attribute and an empty `alt=""`.
**WRONG:**
```html
<!-- Missing alt: accessibility violation, AI agents flag this -->
<img src="decorative-line.svg">
<!-- Alt text on decorative image: AI agents waste time processing irrelevant info -->
<img src="decorative-line.svg" alt="A blue horizontal line separator">
<!-- Alt describes file name instead of content -->
<img src="IMG_2847.jpg" alt="IMG_2847.jpg">
```
**CORRECT:**
```html
<!-- Empty alt for decorative images: correctly excluded from accessibility tree -->
<img src="decorative-line.svg" alt="">
<!-- Descriptive alt for content images -->
<img src="team-photo.jpg" alt="Engineering team at the 2026 company offsite">
<!-- CSS background for purely decorative images (best practice) -->
<div style="background-image: url('decorative-line.svg')"></div>
```
**Rule:** Every `<img>` MUST have an `alt` attribute. Decorative = `alt=""`. Content = descriptive text.
---
## 8. Language Attribute Pitfalls
Common mistakes with the `<html lang>` attribute.
**WRONG:**
```html
<!-- Missing entirely -->
<html>
<!-- Wrong code format -->
<html lang="english">
<html lang="EN_US">
<html lang="eng">
<!-- Language set on body instead of html -->
<html>
<body lang="en">
```
**CORRECT:**
```html
<!-- Valid BCP 47 codes -->
<html lang="en">
<html lang="en-US">
<html lang="nl-NL">
<html lang="de">
<html lang="ja">
<html lang="zh-Hans">
<!-- For multilingual content, set main language on html and override per element -->
<html lang="en">
<body>
<p>This is English content.</p>
<blockquote lang="fr">Ceci est en français.</blockquote>
</body>
</html>
```
**Rule:** Use lowercase two-letter ISO 639-1 codes, optionally with region subtag (e.g., `en-US`).
---
## 9. Heading Hierarchy Skip in CMS Content
CMS editors or markdown renderers that start content with `<h3>` or `<h4>`, skipping levels from the page's `<h1>`.
**WRONG:**
```html
<!-- Page template provides h1 -->
<h1>Blog</h1>
<!-- CMS content starts at h3, skipping h2 -->
<article>
<h3>Introduction to AI Agents</h3> <!-- Skips h2! -->
<p>...</p>
<h4>What are AI agents?</h4>
<p>...</p>
</article>
```
**CORRECT:**
```html
<!-- Page template provides h1 -->
<h1>Blog</h1>
<!-- CMS content uses sequential levels from h2 -->
<article>
<h2>Introduction to AI Agents</h2>
<p>...</p>
<h3>What are AI agents?</h3>
<p>...</p>
</article>
```
**Fix for CMS:** Configure the CMS rich text editor to start at `<h2>`, or use a post-processor to remap heading levels.
---
## 10. Multiple `<main>` Elements
Using `<main>` more than once on a page, or wrapping everything (including header/footer) in `<main>`.
**WRONG:**
```html
<body>
<!-- Multiple mains -->
<main>
<nav>...</nav>
</main>
<main>
<article>...</article>
</main>
<!-- Or: everything inside main -->
<main>
<header>...</header>
<article>...</article>
<footer>...</footer>
</main>
</body>
```
**CORRECT:**
```html
<body>
<header>
<nav>...</nav>
</header>
<main>
<!-- Only the primary content -->
<article>...</article>
</main>
<footer>...</footer>
</body>
```
**Rule:** Exactly one `<main>` per page, containing only the primary content. Header, nav, and footer go outside `<main>`.
---
## 11. Semantic Elements Inside `<div>` Wrappers
Wrapping semantic elements in unnecessary `<div>` containers for styling, which can break implicit ARIA role mapping.
**WRONG:**
```html
<!-- div wrapper breaks header's implicit banner role -->
<div class="header-wrapper">
<header>
<nav>...</nav>
</header>
</div>
<!-- div between body and main -->
<body>
<div class="page-container">
<main>...</main>
</div>
</body>
```
**CORRECT:**
```html
<!-- Apply styles directly to semantic elements -->
<header class="header-wrapper">
<nav>...</nav>
</header>
<!-- If a wrapper is truly needed, keep semantic elements as direct children of body -->
<body>
<header>...</header>
<main class="page-container">...</main>
<footer>...</footer>
</body>
```
**Note:** `<header>` only has the implicit `banner` role when it's a direct child of `<body>`. Nesting it inside a `<div>` can change its semantic meaning in some assistive technologies.
references/semantic-html-guide.md
# Semantic HTML Guide
Complete reference for semantic HTML elements, ARIA landmark mapping, heading hierarchy, and link text patterns used by AI agents to navigate web content.
## Document Outline Structure
AI agents parse pages using the accessibility tree, which is built from semantic HTML elements. The standard document outline:
```html
<body>
<header> <!-- Site banner: logo, primary nav, search -->
<nav> <!-- Primary navigation -->
</header>
<main> <!-- Primary page content (exactly one per page) -->
<article> <!-- Self-contained content unit -->
<header> <!-- Article header (different from site header) -->
<section> <!-- Thematic grouping within the article -->
<section>
<footer> <!-- Article footer: author, date, tags -->
</article>
<aside> <!-- Related but tangential content (sidebar) -->
</main>
<footer> <!-- Site footer: secondary nav, copyright, links -->
<nav> <!-- Footer navigation -->
</footer>
</body>
```
## Semantic Elements Reference
### `<header>`
- **Purpose:** Introductory content for its nearest sectioning ancestor
- **Implicit ARIA role:** `banner` (only when direct child of `<body>`)
- **Usage:** Site header (logo, nav) or article header (title, date)
- **Note:** A page can have multiple `<header>` elements: one for the site, one per `<article>`
```html
<!-- Site header -->
<header>
<a href="/" aria-label="Home">
<img src="logo.svg" alt="Acme Inc.">
</a>
<nav aria-label="Primary">...</nav>
</header>
<!-- Article header -->
<article>
<header>
<h2>Article Title</h2>
<time datetime="2026-02-27">February 27, 2026</time>
</header>
</article>
```
### `<nav>`
- **Purpose:** Major navigation block
- **Implicit ARIA role:** `navigation`
- **Usage:** Primary nav, footer nav, breadcrumbs, table of contents
- **Note:** Use `aria-label` when page has multiple `<nav>` elements
```html
<nav aria-label="Primary">
<a href="/" aria-current="page">Home</a>
<a href="/products">Products</a>
<a href="/about">About</a>
</nav>
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/products">Products</a></li>
<li aria-current="page">Widget Pro</li>
</ol>
</nav>
```
### `<main>`
- **Purpose:** Dominant content of the `<body>`
- **Implicit ARIA role:** `main`
- **Rule:** Exactly ONE `<main>` per page
- **Note:** Must not be a descendant of `<article>`, `<aside>`, `<footer>`, `<header>`, or `<nav>`
```html
<body>
<header>...</header>
<main>
<!-- All primary content goes here -->
</main>
<footer>...</footer>
</body>
```
### `<article>`
- **Purpose:** Self-contained composition that could be independently distributed
- **Implicit ARIA role:** `article`
- **Usage:** Blog post, news story, forum post, product card, comment
- **Test:** Would this content make sense in an RSS feed or shared on its own?
```html
<article>
<header>
<h2>How AI Agents Parse Web Pages</h2>
<time datetime="2026-02-27">Feb 27, 2026</time>
</header>
<p>AI agents rely on the accessibility tree...</p>
<footer>
<p>By <a href="/authors/jane">Jane Doe</a></p>
</footer>
</article>
```
### `<section>`
- **Purpose:** Thematic grouping of content, typically with a heading
- **Implicit ARIA role:** `region` (only when it has an accessible name via `aria-label` or `aria-labelledby`)
- **Usage:** Chapters, tab panels, grouped form fields
- **Rule:** Always include a heading in a `<section>`
```html
<section aria-labelledby="pricing-heading">
<h2 id="pricing-heading">Pricing</h2>
<p>Choose the plan that works for you.</p>
</section>
```
### `<aside>`
- **Purpose:** Content tangentially related to the surrounding content
- **Implicit ARIA role:** `complementary`
- **Usage:** Sidebars, pull quotes, related articles, advertising
```html
<main>
<article>...</article>
<aside aria-label="Related articles">
<h2>Related Articles</h2>
<ul>
<li><a href="/blog/ssr-guide">SSR Implementation Guide</a></li>
<li><a href="/blog/aria-patterns">ARIA Patterns for AI</a></li>
</ul>
</aside>
</main>
```
### `<footer>`
- **Purpose:** Footer for its nearest sectioning ancestor
- **Implicit ARIA role:** `contentinfo` (only when direct child of `<body>`)
- **Usage:** Site footer (copyright, links) or article footer (author, tags)
```html
<!-- Site footer -->
<footer>
<nav aria-label="Footer">
<a href="/privacy">Privacy Policy</a>
<a href="/terms">Terms of Service</a>
</nav>
<p>© 2026 Acme Inc.</p>
</footer>
```
## Article vs Section Decision Guide
| Question | If yes → | If no → |
|----------|----------|---------|
| Could this stand alone (RSS feed, social share)? | `<article>` | `<section>` |
| Is it a blog post, news item, comment, or product? | `<article>` | Consider `<section>` |
| Is it a thematic grouping with a heading? | `<section>` | Consider `<div>` |
| Does it only exist to style/layout content? | `<div>` | Use semantic element |
**Nesting rules:**
- `<article>` can contain `<section>` (chapters within a post)
- `<section>` can contain `<article>` (a "latest posts" section with multiple articles)
- `<article>` can contain `<article>` (comments on a blog post)
```html
<!-- Blog post with sections -->
<article>
<h1>Complete Guide to SSR</h1>
<section>
<h2>What is SSR?</h2>
<p>...</p>
</section>
<section>
<h2>Framework Comparison</h2>
<p>...</p>
</section>
</article>
<!-- Section containing multiple articles -->
<section aria-labelledby="latest-heading">
<h2 id="latest-heading">Latest Posts</h2>
<article><h3>Post One</h3><p>...</p></article>
<article><h3>Post Two</h3><p>...</p></article>
</section>
```
## Implicit ARIA Role Mapping
Semantic HTML elements automatically map to ARIA roles. Using the semantic element is preferred over explicit `role` attributes.
| HTML Element | Implicit ARIA Role | Condition |
|-------------|-------------------|-----------|
| `<header>` | `banner` | Direct child of `<body>` |
| `<nav>` | `navigation` | Always |
| `<main>` | `main` | Always |
| `<footer>` | `contentinfo` | Direct child of `<body>` |
| `<aside>` | `complementary` | Always |
| `<article>` | `article` | Always |
| `<section>` | `region` | Only with accessible name |
| `<form>` | `form` | Only with accessible name |
| `<search>` | `search` | Always |
**Key rule:** Do NOT add redundant `role` attributes to semantic elements:
```html
<!-- WRONG: redundant -->
<main role="main">
<nav role="navigation">
<header role="banner">
<!-- CORRECT: implicit roles are sufficient -->
<main>
<nav>
<header>
```
## Additive ARIA Attributes
These attributes ADD information beyond what semantic HTML provides:
### `aria-label`
Provides an accessible name. Use when multiple elements of the same type exist:
```html
<nav aria-label="Primary">...</nav>
<nav aria-label="Footer">...</nav>
<nav aria-label="Breadcrumb">...</nav>
```
### `aria-labelledby`
Points to a visible heading as the accessible name:
```html
<section aria-labelledby="pricing-heading">
<h2 id="pricing-heading">Pricing Plans</h2>
</section>
```
### `aria-describedby`
Points to additional descriptive text:
```html
<input type="email" aria-describedby="email-hint">
<p id="email-hint">We'll never share your email.</p>
```
### `aria-current`
Indicates the current item in a set:
```html
<nav>
<a href="/" aria-current="page">Home</a>
<a href="/about">About</a>
</nav>
```
Values: `page`, `step`, `location`, `date`, `time`, `true`
### `aria-live`
Announces dynamic content changes to assistive technology:
```html
<div aria-live="polite" aria-label="Search results">
<!-- Content updated via JavaScript -->
</div>
<div aria-live="assertive" role="alert">
<!-- Urgent notifications -->
</div>
```
### `aria-expanded`
Indicates whether a collapsible element is expanded:
```html
<button aria-expanded="false" aria-controls="menu">Menu</button>
<nav id="menu" hidden>...</nav>
```
### `aria-hidden`
Hides decorative content from the accessibility tree:
```html
<span aria-hidden="true">🎉</span>
<img src="decorative-swoosh.svg" alt="" aria-hidden="true">
```
## Heading Hierarchy Patterns
### Standard Page Layout
```html
<h1>Company Name: Page Title</h1>
<h2>About Us</h2>
<h3>Our Mission</h3>
<h3>Our Team</h3>
<h2>Services</h2>
<h3>Web Development</h3>
<h3>AI Integration</h3>
<h2>Contact</h2>
```
### Blog Post
```html
<h1>How to Implement SSR for AI Agents</h1>
<h2>Introduction</h2>
<h2>Why SSR Matters</h2>
<h3>AI Agent Limitations</h3>
<h3>Performance Benefits</h3>
<h2>Implementation Guide</h2>
<h3>Next.js</h3>
<h3>Nuxt</h3>
<h3>Astro</h3>
<h2>Conclusion</h2>
```
### E-commerce Product Page
```html
<h1>Wireless Noise-Cancelling Headphones</h1>
<h2>Product Description</h2>
<h2>Specifications</h2>
<h3>Audio</h3>
<h3>Battery</h3>
<h3>Connectivity</h3>
<h2>Reviews</h2>
<h3>Customer Review by John</h3>
<h3>Customer Review by Sarah</h3>
```
## Descriptive Link Text Patterns
### Generic → Descriptive Replacements
| Generic (avoid) | Descriptive (use instead) |
|-----------------|--------------------------|
| Click here | View pricing plans |
| Read more | Read the SSR implementation guide |
| Learn more | Explore our API documentation |
| Here | Download the annual report |
| Link | Visit the W3C accessibility standards |
| More | See all 24 customer reviews |
| Details | View order #1234 details |
| Info | Read the product specifications |
### Context-Aware Patterns
```html
<!-- Blog listing -->
<article>
<h3>Implementing SSR for AI Agents</h3>
<p>Learn how server-side rendering improves AI agent comprehension...</p>
<a href="/blog/ssr-for-ai">Read "Implementing SSR for AI Agents"</a>
</article>
<!-- Product cards -->
<div>
<h3>Widget Pro</h3>
<p>$99/month</p>
<a href="/products/widget-pro">View Widget Pro details</a>
</div>
<!-- Documentation -->
<p>
Configure your server using the
<a href="/docs/configuration">server configuration guide</a>.
</p>
```
references/ssr-strategies.md
# SSR Strategies by Framework
Server-side rendering (SSR) ensures AI agents can read your content without executing JavaScript. This guide covers implementation strategies for major frameworks.
## Why SSR Matters for AI Agents
AI crawlers (GPTBot, ClaudeBot, PerplexityBot, Googlebot) fetch raw HTML. They do **not** execute JavaScript. A client-side rendered SPA returns an empty shell:
```html
<!-- What AI agents see from a typical SPA -->
<html>
<body>
<div id="root"></div>
<script src="/bundle.js"></script>
</body>
</html>
```
This fails checkpoint 3.1. The fix: render content on the server.
## Detecting SPA Shell Problems
### Quick test with curl
```bash
# See what AI agents see (raw HTML, no JS)
curl -s https://example.com | head -50
# Count visible text characters (excluding scripts/styles/tags)
curl -s https://example.com \
| sed 's/<script[^>]*>.*<\/script>//g; s/<style[^>]*>.*<\/style>//g; s/<[^>]*>//g' \
| tr -s ' \n' \
| wc -c
```
If the character count is under 200, the site relies on JavaScript for content.
### Common SPA shell indicators
```html
<div id="root"></div> <!-- React (Create React App) -->
<div id="app"></div> <!-- Vue CLI, generic SPAs -->
<div id="__next"></div> <!-- Next.js with client-only rendering -->
<div id="__nuxt"></div> <!-- Nuxt with SPA mode -->
```
## Framework Guides
### Next.js
#### App Router (Next.js 13+): Server Components by Default
Components in the `app/` directory are Server Components by default. Content is rendered on the server automatically.
```tsx
// app/page.tsx: Server Component (default, no directive needed)
export default async function HomePage() {
const products = await fetch('https://api.example.com/products').then(r => r.json());
return (
<main>
<h1>Our Products</h1>
{products.map((product: Product) => (
<article key={product.id}>
<h2>{product.name}</h2>
<p>{product.description}</p>
</article>
))}
</main>
);
}
```
For static generation with known paths:
```tsx
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map(post => ({ slug: post.slug }));
}
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
return (
<article>
<h1>{post.title}</h1>
{/* Render sanitized HTML content */}
<div>{post.content}</div>
</article>
);
}
```
Only add `"use client"` when the component needs browser APIs (useState, useEffect, event handlers).
#### Pages Router (Next.js 12 and earlier)
```tsx
// pages/products.tsx
import type { GetServerSideProps } from 'next';
interface Props {
products: Product[];
}
export const getServerSideProps: GetServerSideProps<Props> = async () => {
const products = await fetchProducts();
return { props: { products } };
};
export default function ProductsPage({ products }: Props) {
return (
<main>
<h1>Products</h1>
{products.map(product => (
<article key={product.id}>
<h2>{product.name}</h2>
<p>{product.description}</p>
</article>
))}
</main>
);
}
```
For static pages that don't change often:
```tsx
export const getStaticProps: GetStaticProps<Props> = async () => {
const products = await fetchProducts();
return {
props: { products },
revalidate: 3600, // ISR: regenerate every hour
};
};
```
### Nuxt (Vue)
#### Nuxt 3: SSR by Default
```ts
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true, // This is the default
});
```
```vue
<!-- pages/products.vue -->
<script setup lang="ts">
const { data: products } = await useFetch('/api/products');
</script>
<template>
<main>
<h1>Products</h1>
<article v-for="product in products" :key="product.id">
<h2>{{ product.name }}</h2>
<p>{{ product.description }}</p>
</article>
</main>
</template>
```
**Fix SPA-mode Nuxt:** If `ssr: false` is set in config, change it to `true`:
```ts
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true, // Changed from false
});
```
### Astro
Astro renders to static HTML by default. No configuration needed for SSR of static content.
```astro
---
// src/pages/products.astro
const products = await fetch('https://api.example.com/products').then(r => r.json());
---
<html lang="en">
<body>
<main>
<h1>Products</h1>
{products.map((product: Product) => (
<article>
<h2>{product.name}</h2>
<p>{product.description}</p>
</article>
))}
</main>
</body>
</html>
```
For dynamic routes with SSR:
```ts
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'hybrid', // Static by default, opt-in SSR per page
adapter: node({ mode: 'standalone' }),
});
```
```astro
---
// src/pages/products/[id].astro
export const prerender = false; // SSR for this page
const { id } = Astro.params;
const product = await fetch(`https://api.example.com/products/${id}`).then(r => r.json());
---
<article>
<h1>{product.name}</h1>
<p>{product.description}</p>
</article>
```
### Remix
Remix uses server-side `loader` functions. All routes are SSR by default.
```tsx
// app/routes/products.tsx
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
export async function loader({ request }: LoaderFunctionArgs) {
const products = await fetchProducts();
return json({ products });
}
export default function Products() {
const { products } = useLoaderData<typeof loader>();
return (
<main>
<h1>Products</h1>
{products.map(product => (
<article key={product.id}>
<h2>{product.name}</h2>
<p>{product.description}</p>
</article>
))}
</main>
);
}
```
### SvelteKit
```ts
// src/routes/products/+page.server.ts
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => {
const products = await fetchProducts();
return { products };
};
```
```svelte
<!-- src/routes/products/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<main>
<h1>Products</h1>
{#each data.products as product}
<article>
<h2>{product.name}</h2>
<p>{product.description}</p>
</article>
{/each}
</main>
```
Ensure SSR is not disabled in config:
```ts
// svelte.config.js
const config = {
kit: {
// Do NOT set: ssr: false
}
};
```
### Angular (with SSR)
```bash
# Add SSR to an existing Angular project
ng add @angular/ssr
```
```ts
// server.ts is generated automatically
// Components render on the server by default with Angular SSR
// For data fetching, use a resolver:
// app/products/products.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { ProductService } from './product.service';
export const productsResolver: ResolveFn<Product[]> = () => {
return inject(ProductService).getProducts();
};
```
```ts
// app/products/products.routes.ts
export const routes: Routes = [
{
path: '',
component: ProductsComponent,
resolve: { products: productsResolver },
},
];
```
## Noscript Fallback
If full SSR is not feasible, add a `<noscript>` fallback for partial credit:
```html
<body>
<div id="root"></div>
<noscript>
<main>
<h1>Example Site</h1>
<p>This site requires JavaScript to function fully.</p>
<nav>
<h2>Site Map</h2>
<ul>
<li><a href="/products">Products</a></li>
<li><a href="/about">About Us</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
<p>For the best experience, please enable JavaScript.</p>
</main>
</noscript>
<script src="/bundle.js"></script>
</body>
```
**Important:** `<noscript>` is a partial solution. AI agents that parse raw HTML will see the noscript content but miss your actual dynamic content. Full SSR is always preferred.
## Verification
After implementing SSR, verify with curl:
```bash
# Check visible text count (should be >200 characters)
curl -s https://example.com \
| sed 's/<script[^>]*>.*<\/script>//g; s/<style[^>]*>.*<\/style>//g; s/<[^>]*>//g' \
| tr -s ' \n' \
| wc -c
# Check for SPA shell (should return no results)
curl -s https://example.com | grep -E '<div id="(root|app|__next)"></div>'
# View the rendered HTML structure
curl -s https://example.com | grep -oE '<(h[1-6]|header|nav|main|article|section|footer)[^>]*>' | head -20
```
SKILL.md
---
name: content-semantics
description: Fixes content and semantic HTML issues. Adds server-side rendering, a correct heading hierarchy, semantic landmark elements, ARIA labels, image alt text, the lang attribute, descriptive link texts, form labels, autocomplete tokens, keyboard access, document titles, button names, correct aria-hidden use and widget state so AI agents can read and operate a page through the HTML and the accessibility tree. Use when asked to "fix semantic HTML", "add SSR", "fix heading hierarchy", "add alt text", "improve semantics score", "fix accessibility for AI", "fix form labels", "add autocomplete", "fix keyboard navigation", "fix button names", "fix aria-hidden", "add aria-expanded", or any semantic HTML task.
---
# Content & Semantics
Fixes the Content & Semantics category of [IsAgentReady.com](https://isagentready.com). Agents read the server HTML, the DOM and the accessibility tree. Good semantic HTML gives reliable signals in each of these views.
- Methodology version: `2026-10-30`. Methodology: https://isagentready.com/en/methodology
- Scoring (weights and points): https://isagentready.com/api/v2/methodology/2026-10-30/scoring
- Full checkpoint data (stage, evidence type, verify command, rescan command): [references/checkpoints.md](references/checkpoints.md)
## When to Use
- The scan shows failures in Content & Semantics (legacy ids 3.x).
- The page is an empty SPA shell without server-rendered text.
- Headings, landmarks, alt texts, labels or button names are missing.
- Forms or widgets are hard for browser agents to operate.
## When NOT to Use
- JSON-LD or FAQPage markup: use `structured-data`.
- robots.txt, sitemap or llms.txt: use `ai-content-discovery`.
- WebMCP tool annotations: use `agent-protocols`.
- Visual design changes without a change to the HTML structure.
## Instructions
1. Get the failing checkpoints from the scan. Each result has a legacy id (`3.2`) and a stable id (`ia.heading-hierarchy`).
2. Go to the fix workflow with the anchor that is equal to the stable id.
3. Adapt the example to the site. Replace `example.com` with the domain of the site.
4. Run the verify command. All verify commands are read-only. Some checks have no shell check. Then use the rescan command.
5. Rescan with `isagentready results <domain> --checkpoint <stable-id>` or at [isagentready.com](https://isagentready.com).
If `$ARGUMENTS` is provided, use it as the URL to fix or as the checkpoint to fix.
## Checkpoints
Strength is the agentready.org spec level (MUST, SHOULD, MAY). Strength does not change the score.
<!-- checkpoints:start (generated by scripts/sync-checkpoints.mjs) -->
| Legacy | Stable ID | Name | AR IDs | Strength |
|---|---|---|---|---|
| 3.1 | [`ia.server-rendered-content`](#ia.server-rendered-content) | Server-side rendered content | AR-READ-01, AR-READ-03 | MUST |
| 3.2 | [`ia.heading-hierarchy`](#ia.heading-hierarchy) | Heading hierarchy | AR-READ-03 | MUST |
| 3.3 | [`ia.semantic-html`](#ia.semantic-html) | Semantic HTML elements | none | MUST |
| 3.4 | [`ia.landmarks-aria`](#ia.landmarks-aria) | Landmarks and ARIA | none | SHOULD |
| 3.5 | [`ia.image-alt`](#ia.image-alt) | Image alt text | none | MUST |
| 3.6 | [`ia.lang-attribute`](#ia.lang-attribute) | Language attribute | none | MUST |
| 3.7 | [`ia.link-text`](#ia.link-text) | Descriptive link texts | none | MUST |
| 3.9 | [`ia.form-labels`](#ia.form-labels) | Form label coverage | none | SHOULD |
| 3.10 | [`ia.form-autocomplete`](#ia.form-autocomplete) | Form field autocomplete | none | SHOULD |
| 3.11 | [`ia.keyboard-navigation`](#ia.keyboard-navigation) | Keyboard navigability | none | SHOULD |
| 3.12 | [`ia.document-title`](#ia.document-title) | Document title | AR-READ-03 | MUST |
| 3.13 | [`ia.button-names`](#ia.button-names) | Button accessible names | none | SHOULD |
| 3.14 | [`ia.aria-hidden`](#ia.aria-hidden) | aria-hidden misuse | none | SHOULD |
| 3.15 | [`ia.widget-state`](#ia.widget-state) | Dynamic widget state | none | SHOULD |
<!-- checkpoints:end -->
## Fix Workflows
<a id="ia.server-rendered-content"></a>
### 3.1 Server-side rendered content
Passes when the `<body>` of the raw HTML has more than 200 characters of visible text without JavaScript. A `<noscript>` fallback gives partial credit.
1. Find an empty shell such as `<div id="root"></div>` in the raw HTML.
2. Turn on SSR or static generation in the framework. See [references/ssr-strategies.md](references/ssr-strategies.md) for Next.js, Nuxt, Astro, Remix, SvelteKit and Angular.
```js
// Next.js App Router: a server component renders on the server by default
export default async function Page() {
const product = await getProduct();
return <main><h1>{product.name}</h1><p>{product.summary}</p></main>;
}
```
Verify: `curl -sf https://example.com/ | sed -e 's/<[^>]*>//g' | tr -s ' \n' | wc -c` (expect more than 200)
<a id="ia.heading-hierarchy"></a>
### 3.2 Heading hierarchy
Passes when the page has exactly one `<h1>`, does not skip levels and has no empty headings.
1. Keep one `<h1>` for the page title. Change other `<h1>` elements to `<h2>`.
2. Add the missing level when a heading jumps from `<h1>` to `<h3>`:
```html
<h1>Planner Pro</h1>
<h2>Features</h2>
<h3>Timeline view</h3>
<h2>Pricing</h2>
```
3. Do not put the logo image in the `<h1>`.
Verify: no read-only shell check gives a pass or fail. Use the rescan command.
<a id="ia.semantic-html"></a>
### 3.3 Semantic HTML elements
Passes when the page has `<header>`, `<nav>`, `<main>`, `<article>` or `<section>`, and `<footer>`.
1. Replace the layout `<div>` wrappers with semantic elements:
```html
<body>
<header><nav aria-label="Primary"><a href="/">Home</a></nav></header>
<main><article><h1>Page title</h1><section><h2>Part</h2></section></article></main>
<footer><p>Example Inc</p></footer>
</body>
```
Verify: `curl -sf https://example.com/ | grep -oE '<(header|nav|main|article|section|footer)[ >]' | sort -u`
<a id="ia.landmarks-aria"></a>
### 3.4 Landmarks and ARIA
Passes when the page has three or more landmark regions and one or more ARIA attributes such as `aria-label`, `aria-current` or `aria-live`.
1. Use the semantic elements from 3.3 first. They give implicit landmarks.
2. Label repeated landmarks and mark the current page:
```html
<nav aria-label="Primary"><a href="/" aria-current="page">Home</a></nav>
<nav aria-label="Footer"><a href="/privacy">Privacy</a></nav>
```
3. Do not add redundant roles such as `<main role="main">`.
Verify: `curl -sf https://example.com/ | grep -oE '(role|aria-[a-z]+)="' | sort -u`
<a id="ia.image-alt"></a>
### 3.5 Image alt text
Passes when each `<img>` has an `alt` attribute. A page without images passes. `alt=""` is correct for decorative images.
1. Write a short description for each content image. Use `alt=""` for decorative images:
```html
<img src="team.jpg" alt="The Example Inc team at the 2026 offsite">
<img src="divider.svg" alt="">
```
Verify: `curl -sf https://example.com/ | grep -oE '<img [^>]*>' | grep -vc 'alt='` (expect 0)
<a id="ia.lang-attribute"></a>
### 3.6 Language attribute
Passes when `<html>` has a `lang` attribute with a valid BCP 47 code such as `en`, `nl` or `en-US`.
1. Set the language on the root element: `<html lang="en">`.
2. Use `lang` on elements in a different language: `<blockquote lang="fr">`.
Verify: `curl -sf https://example.com/ | grep -oE '<html[^>]* lang="[A-Za-z-]+"'`
<a id="ia.link-text"></a>
### 3.7 Descriptive link texts
Passes when the sampled links do not use generic text such as "click here", "read more", "learn more", "here", "more", "details" or "info".
1. Replace generic text with text that names the target:
```html
<a href="/pricing">See the pricing plans</a>
<a href="/blog/sprints" aria-label="Read the sprint planning guide">Read more</a>
```
Verify: `curl -sf https://example.com/ | grep -ciE '>(click here|read more|learn more|here|more)</a>'` (expect 0)
<a id="ia.form-labels"></a>
### 3.9 Form label coverage
Passes when each visible form field has a label: `<label for>`, a wrapping `<label>`, `aria-label`, `aria-labelledby`, `title` or `placeholder`. A page without fields skips this check.
1. Connect a label to each field:
```html
<label for="email">Email address</label>
<input id="email" name="email" type="email">
```
2. Use a visible `<label>` when possible. A `placeholder` alone is a weak label.
Verify: no read-only shell check gives a pass or fail. Use the rescan command.
<a id="ia.form-autocomplete"></a>
### 3.10 Form field autocomplete
Passes when 80% or more of the text, email, tel, url, password and search fields have an `autocomplete` attribute. 40% or more gives partial credit.
1. Add the correct token to each field:
```html
<input name="name" autocomplete="name">
<input name="email" type="email" autocomplete="email">
<input name="phone" type="tel" autocomplete="tel">
<input name="password" type="password" autocomplete="current-password">
```
Verify: `curl -sf https://example.com/ | grep -oE '<input[^>]*type="(text|email|tel|url|password)"[^>]*>' | grep -vc 'autocomplete='` (expect 0)
<a id="ia.keyboard-navigation"></a>
### 3.11 Keyboard navigability
Passes when the page has no positive `tabindex`, no clickable `<div>` without a role and focus, and no event handler on a `<div>` or `<span>` without a role.
1. Remove positive `tabindex` values. Use `tabindex="0"` or the natural order.
2. Use native controls for click targets:
```html
<!-- Incorrect --> <div onclick="openMenu()">Menu</div>
<!-- Correct --> <button type="button" onclick="openMenu()">Menu</button>
```
Verify: `curl -sf https://example.com/ | grep -cE 'tabindex="[1-9]'` (expect 0)
<a id="ia.document-title"></a>
### 3.12 Document title
Passes when `<title>` is present, not generic ("Home", "Untitled") and 10 to 70 characters long.
1. Give each page a unique title with the page topic and the brand:
```html
<title>Pricing plans for teams | Planner Pro</title>
```
Verify: `curl -sf https://example.com/ | grep -oE '<title>[^<]*</title>'`
<a id="ia.button-names"></a>
### 3.13 Button accessible names
Passes when each `<button>`, `input` button and `[role="button"]` has a name from text, `aria-label`, `aria-labelledby`, `title` or `value`.
1. Add text or an `aria-label` to icon buttons:
```html
<button type="button" aria-label="Close dialog"><svg aria-hidden="true">...</svg></button>
<input type="submit" value="Send message">
```
Verify: no read-only shell check gives a pass or fail. Use the rescan command.
<a id="ia.aria-hidden"></a>
### 3.14 aria-hidden misuse
Passes when `aria-hidden="true"` is not on `<body>` and not on a container with links, buttons or inputs. `aria-hidden` on `<body>` fails.
1. Remove `aria-hidden` from `<body>` and from containers with focusable elements.
2. To hide inactive content, use `hidden` or `inert`. These attributes also remove focus:
```html
<div id="old-panel" inert>...</div>
```
Verify: `curl -sf https://example.com/ | grep -c '<body[^>]*aria-hidden="true"'` (expect 0)
<a id="ia.widget-state"></a>
### 3.15 Dynamic widget state
Applies when the server HTML has widgets or live regions. Passes when each widget exposes its state and updates are announced.
1. Expose the state on disclosures, tabs, toggles and the current item:
```html
<button aria-expanded="false" aria-controls="faq-1">Can I cancel?</button>
<div id="faq-1" hidden>Yes, at any time.</div>
<div role="tablist">
<button role="tab" aria-selected="true" aria-controls="tab-1">Monthly</button>
</div>
<p role="status" aria-live="polite">3 results</p>
```
2. Update `aria-expanded` and `aria-selected` in the same script that changes the view.
Verify: `curl -sf https://example.com/ | grep -oE 'aria-(expanded|selected|checked|pressed|current|live)=' | sort | uniq -c`
## Key Gotchas
1. Each component adds its own `<h1>`, so the page has many.
2. `<img src="...">` without `alt` fails. `alt=""` is correct for decoration.
3. A `<div id="root"></div>` shell has no text for agents.
4. `aria-hidden="true"` on a modal backdrop that contains the modal hides the modal.
5. A JavaScript toggle changes the view but not `aria-expanded`.
See [references/gotchas.md](references/gotchas.md) for correct and incorrect examples.
## References
- [references/checkpoints.md](references/checkpoints.md): generated checkpoint data for each checkpoint
- [references/semantic-html-guide.md](references/semantic-html-guide.md): semantic elements, ARIA mapping, heading and link text patterns
- [references/ssr-strategies.md](references/ssr-strategies.md): SSR for each framework
- [references/gotchas.md](references/gotchas.md): common errors with correct and incorrect examples