DEV Community

Daniel Pertu
Daniel Pertu

Posted on

All 516 of our pages have exactly one h1, no skip link, and skip a heading level in exactly the same place

A template-driven site does not have 516 pages. It has about six pages and 516 URLs, which means every accessibility defect is either on one page or on all of them. That is a cheerful thought, because it makes a census worth running: twenty lines of Python, every URL in the sitemap, count the tags.

This is not a substitute for axe or for actually tabbing through a page with a screen reader on. It answers a narrower question, which is whether the site is doing the same thing everywhere, and that question is the one a build-time template site should ask first.

Here is what came back for munchable.app, all 516 URLs, every one a 200.

The part that is fine

pages fetched                516
exactly one <h1>             516
exactly one <main>           516
exactly one <footer>         516
html lang set                516
<img> elements                 0   so: images missing alt text, 0
labelled <nav> elements      508 pages have 2, 8 pages have 1
Enter fullscreen mode Exit fullscreen mode

The eight pages with one <nav> are the top-level ones that have no breadcrumb. The two labels are Primary and Breadcrumb. There are no images anywhere on the site, which is a design decision rather than an accessibility strategy, and it does mean the single most common accessibility defect on the web cannot occur here.

A census should report the clean rows too, otherwise it is not a census, it is a complaint.

5,720 headings, and 2,064 of them are four words

total headings across the site   5,720
pages with exactly four <h4>       516
pages with zero <h3>               502
pages that skip a heading level    516
Enter fullscreen mode Exit fullscreen mode

Every page in the site has four <h4> elements and almost none has an <h3>, so every page goes from h2 straight to h4, exactly once, 516 times.

The four h4s are the footer:

<h4>Product</h4>
<h4>Guides</h4>
<h4>Company</h4>
<h4>Legal</h4>
Enter fullscreen mode Exit fullscreen mode

Four column headings in one shared component. 2,064 of the 5,720 headings on this site, 36 per cent of them, are those four words repeated once per URL.

I want to be accurate about how much this matters, because "heading level skip" sounds worse than it is and overstating it is how people learn to ignore accessibility findings. Nobody is blocked. A screen reader user navigating by heading gets a list, and a jump from level 2 to level 4 in that list implies a level 3 section that was not announced, so the structure reads as though something is missing from it. It is a systematic small inaccuracy, repeated on every page of the site, and the fix is editing one file.

The fix is also not obvious, which is why it has survived. The footer headings are visually small, and the level follows the size, which is the usual way this happens. But level is structure, not size: these are four sibling sections of the page footer, and they belong at the same level as the page's other top-level sections, which is h2, with the type size set in CSS where it belongs.

No skip link, and the honest measurement of what that costs

Zero of the 516 pages have a skip link. Before writing that up as a failure, I counted what a keyboard user actually has to pass to reach the content:

focusable links before <main>: 5
  /          the brand
  /#why
  /#how
  /#faq
  /#pricing
Enter fullscreen mode Exit fullscreen mode

Five. Not thirty, not a mega menu, five. A skip link is still worth adding and it costs nothing, but a census will happily rank this above the heading structure, and the heading structure affects more people more often.

The other thing in that list is worth noticing on its own. On 515 of our 516 pages, four of the five header links are fragment links to a different page. From a recipe, "FAQ" is not an anchor on this page, it is a navigation to the homepage followed by a jump. In the tab order those are indistinguishable from same-page anchors, and the only hint a reader gets is that the page changed under them.

The button that is not in any of the 516 documents

<button> elements per page: 3 on 512 pages, 0 on 3, 7 on 1
Enter fullscreen mode Exit fullscreen mode

The three are all in the closing call to action: two store pills that say "coming soon" and one live button for the web app. The three pages with none are the legal pages, which have no CTA. The seven are on the homepage.

What is interesting is what is absent. Our header has a Sign in control, and it is in none of those counts, because the component that renders it returns nothing until it knows whether you have a session:

// Reserve no space before the session is known; the header reflows once.
if (!ready) return <div className="auth-controls" aria-hidden />;
Enter fullscreen mode Exit fullscreen mode

So in the HTML of all 516 pages there is no sign-in affordance at all. It appears after hydration, and the JavaScript it is waiting for is the chunk I measured earlier this week: 259 KB containing a full auth client. Before that lands, a keyboard user tabbing the header has no account control to find, and no indication that one is coming, because the placeholder is aria-hidden and empty.

That is the one on this list I would call a real defect rather than tidying. The page is otherwise entirely usable without JavaScript, and the single interactive affordance in the chrome is the one thing that needs it.

Two smaller things the count turned up:

  • The two store pills carry aria-disabled="true" without disabled, deliberately, so they stay focusable and can show their "coming soon" tip on focus rather than only on hover. That is the right pattern for the behaviour, and the price is two tab stops per page that do nothing when pressed. Named, accepted.
  • Every page declares lang="en" in the HTML and inLanguage: "en-GB" in its structured data. We write British English, so the HTML is the one that is wrong, in the root layout, one attribute.

Run it on your own site

Fetch your sitemap, fetch every URL in it, and count, per page: h1 elements, the heading level sequence, main, nav, img without alt, focusables before main. Print the distribution rather than the average. The average hides the thing you want, which is uniformity: a site where 514 pages do one thing and 2 do another has a bug in a route, and a site where all 516 do the same wrong thing has a bug in a component, and those are completely different afternoons of work.

Ours was the second kind, four times over. If you want to check the heading structure yourself, any answer page will do, because they are all the same page.

Top comments (0)