fix(a11y): add semantic headings and landmarks to the document structure - #2091
fix(a11y): add semantic headings and landmarks to the document structure#2091LeonarddeR wants to merge 13 commits into
Conversation
The page header block emitted a flat sequence of siblings with no landmark, so assistive technology had no way to identify the title, metadata and description as introductory content. Wrap breadcrumb through description in `<header>` across the three header templates. The TOC dropdown stays outside, as it belongs with the content region. The element lands inside `<main>`, so per spec it is a generic section header rather than a second `banner` landmark. Verified layout-neutral: element geometry across the affected pages is byte-identical before and after. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Taxonomy and tag pages rendered their title as `<p class="display-4">`, leaving every one of them without an `h1`. Promote it to `<h1>` and add `mb-3` to keep the paragraph's 1rem bottom margin, which a heading would otherwise reduce to 0.5rem. Fixes four English pages plus their French and Dutch equivalents. Verified layout-neutral. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The 404 page had no `h1`, and its two paragraphs were nested inside a `<span>` — invalid markup, since a span is phrasing content and cannot contain flow content. Change the wrapper to `<div>` and promote the numeral to `<h1>`, keeping `mb-3` so the heading retains the paragraph's 1rem bottom margin. The translated message stays a paragraph. Verified layout-neutral. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same defect as the 404 page: no `h1`, and a paragraph nested inside a `<span>`. Change the wrapper to `<div>` and promote the title to `<h1>`. Because `fs-3` only sets a font size, the heading also needs `lh-base` to keep the paragraph's 1.5 line height and `fw-bold` to keep the weight it previously inherited from the wrapper. Verified layout-neutral. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The "On this page" label was a `<strong>`, so it never appeared in a screen reader's heading list and could not be jumped to. Its `<nav>` was also unnamed, leaving it competing with the navbar and the two breadcrumb landmarks. Promote the label to `<h6>` and point the nav at it with `aria-labelledby`. Reuses the existing `toc` translation, so no new i18n keys. The utility classes reproduce the previous appearance exactly: `lh-base` restores the 1.5 line height a heading would drop to 1.2, and `mt-0` cancels the top margin the theme applies to headings. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The standard nav branch derived `aria-selected` from the loop index instead of `$show`, the same variable that already drives the `active` class. With `nav-show` pointing at any tab other than the first, two tabs disagreed: one looked selected and a different one reported itself as selected. The buttons branch already used `$show`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
👷 Deploy request for gethinode-demo pending review.Visit the deploys page to approve it
|
An h1 containing only "404" announces as a bare number. Move both the numeral and the translated message into the heading so it reads "404 Page not found", and carry the visual styling on block-level spans. Three classes keep the rendering identical: `d-block` restores the block layout the paragraphs had, `lh-base` keeps the message at line height 1.5 rather than the 1.2 a heading would impose, and `fw-bold` is repeated on the numeral because `display-1` sets its own font weight on the element, which an inherited value cannot override. Verified against the previous markup at the same viewport: element geometry is identical, and Lighthouse still reports 95 with only the pre-existing `link-in-text-block` finding on the unrelated trailing link. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Reviewed by building the branch and inspecting the rendered exampleSite rather than reading the diff alone. One blocking item, two verification gaps, everything else checks out. Verified
The four visual-parity catches (heading Blocking: the TOC
|
The `h6` followed an `h2` or `h3` in document order, a jump of three to four levels that axe-core's heading-order rule fails. It went unnoticed because the TOC column is `d-none` below the `md` breakpoint, so Lighthouse's mobile emulation never audited it. Bootstrap's `.h6` class keeps the 1rem font size, while the `h2` element makes the step a decrease, which the rule permits. Verified across the exampleSite: 66 violating pages before, 0 after. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`layouts/form/single.html` rendered zero pages in the exampleSite, so its heading and nesting fix shipped without build coverage. Add a page routing through it. Every existing nav fixture put `show="true"` on the first item, where `eq $index 0` and `$show` agree, so the aria-selected fix had no regression guard. A three-tab nav whose third item is shown produces two active/aria-selected mismatches on the previous markup and none on the current. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Both commits are up: 9d1b86c (TOC heading level) and 6c134ae (fixtures). TOC heading — reproduced the violation before changing anything: 66 of 66 pages carrying the TOC heading failed
Full sweep after both commits: 0 heading-order violations, 80 🤖 Generated with Claude Code |
What
Phase 1 of #1519 — document structure and landmarks only. Every page gets exactly one
h1, the page header becomes a real<header>, and the table of contents label becomes a heading that can be jumped to.No new component arguments, no vendored module changes, and no new i18n keys.
Baseline: 15 English pages had no
h1at all, and<header>appeared nowhere inlayouts/. After this PR that's 7 pages without anh1, and all 7 are the known follow-up work (see below).Fixes (one commit each)
<header>, ending before the TOC dropdownheader.html,docs/header.html,minimal/header.html<p class="display-4">→<h1>(4 EN pages + FR/NL)tags/list.html<span>→<div>(a span cannot contain<p>) and the title →<h1>404.html<h1>form/single.html<strong>→<h6 id="toc-heading">, nav getsaria-labelledbyassets/toc-parse-content.htmlaria-selectedderived from$showinstead of the loop indexassets/nav.html404.htmlThe
<header>lands inside<main>, so per spec it is a generic section header, not a secondbannerlandmark.The TOC change reuses the existing
toctranslation, already present in all 8 locales.On the 404 page, both the numeral and
pageNotFoundTitlesit inside theh1as block-level spans, so it announces as "404 Page not found" rather than a bare number.The
nav.htmlfix is a genuine bug: withnav-showpointing at any tab other than the first, theactiveclass landed on one tab whilearia-selected="true"stayed on tab 0. Thebuttonsbranch already used$show.On the nav bullet in the issue
The issue's checklist proposes replacing
ul/liwithnav/ainassets/nav.htmlper the Bootstrap base-nav example. That change should not be made.assets/nav.htmlis not navigation — it is an ARIA tab widget (role="tablist"on theul,role="presentation"on theli,role="tab"on the button, paired withrole="tabpanel"innav-item.html), and its only caller is the{{< nav >}}shortcode, where everytab-typeis a tab variant. Converting it would breakaria-required-children— the same audit_shortcodes/button-group.htmlwas already fixed for in #2066 — and would breakassets/js/nav.js, which walksparentElement.parentElementto reachul[data-companion]; without the<li>that resolves to the wrapping<div>,data-companionreturnsnull, and the responsive dropdown sync silently stops working.Verification
Lighthouse accessibility — blog post, docs page, tags, id-demo, form and minimal layouts all score 100 with zero failures. The 404 page scores 95 on
link-in-text-block, which is pre-existing and unrelated: the failing node is the trailing "home page" link (2:1 contrast against surrounding text, no underline), and re-auditing the pre-change template returns the identical 95 with the identical single failure. On that same pair,heading-orderwent fromn/a(the page had no headings) to pass.Visual parity — the changes are semantics-only, so every affected page was fingerprinted in the browser before and after: each text-bearing element in
<main>recorded as text plus x/y/width/height and hashed. All 8 pages hash byte-identical to the pre-change baseline, and the reworked 404 heading was re-checked the same way against the original markup at a matched viewport.That check earned its keep — it caught four regressions that would otherwise have shipped:
mb-3fs-3sets only a font size, so the formh1line-height dropped 39.35px → 31.48px →lh-basemt-0.display-1setsfont-weighton the element itself, so the 404 numeral could not inheritfw-boldfrom theh1→ repeated on the spanStructural assertions — a throwaway script over the built exampleSite (en/fr/nl) checking one
h1per non-stub page, the TOC heading and its named landmark,<header>presence, no flow content inside<span>, andactive/aria-selectedagreement on every tab. All five red before, all five green after.Two page types are unreachable in the exampleSite (
type: form,type: minimal), so they were verified with temporary local fixtures that are not part of this PR —EXPECTED_PAGESinscripts/check-build-determinism.shis deliberately untouched.pnpm lintpasses;pnpm build:exampleis clean with no new i18n warnings and noerrorffromtoc-headings.Follow-up (not in this PR)
The 7 remaining pages without an
h1fall into two groups:docs/blocksanddocs/componentscome fromsection-title.htmlwithuse-title: false, and home plus team arecontent_blockspages owned by mod-blocks.Promoting in-content component titles (cards, section titles, timeline, accordion) needs a
heading-levelargument, and has a blocker to solve first:toc-headings.htmlregex-scrapes.Contentfor<h2>–<h3>to build the mobile TOC while the desktop TOC reads.Page.Fragments, so new in-content headings would make the two disagree — andtoc-headings.htmlcallserrorfon any heading without anid, which would fail the build.🤖 Generated with Claude Code