CogniPrep's root layout opens with the right instinct:
<a
href="#main-content"
className="focus:bg-primary focus:text-primary-foreground sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:rounded-md focus:px-4 focus:py-2"
>
Skip to main content
</a>
Visually hidden, revealed on focus, first in the document, so the first Tab on any page offers to jump past the header. It is in the root layout, so it is on every route the app serves. Including all 375 in the sitemap.
Now grep for the thing it points at:
$ grep -rn 'id="main-content"' app components
app/not-found.tsx:22: id="main-content"
app/dashboard/layout.tsx:92: <main id="main-content" className="pt-14">
app/_components/RouteErrorFallback.tsx:87: id="main-content"
app/maintenance/page.tsx:36: id="main-content"
Four. The signed-in dashboard, the 404 page, the error boundary, and the maintenance page. The skip link works when you are logged in, when you have mistyped a URL, when something has crashed, and when the site is down for maintenance. On every public page it points at nothing.
Checkable from outside
The link is in the served HTML and so is the absence:
$ for p in "" games pricing blogs tests cheating interview about help comparison; do
echo -n "/$p "
curl -s "https://cogniprep.app/$p" | grep -c 'id="main-content"'
done
/ 0
/games 0
/pricing 0
/blogs 0
/tests 0
/cheating 0
/interview 0
/about 0
/help 0
/comparison 0
Ten pages, ten zeroes, and a href="#main-content" on each one. A fragment that matches no element is not an error in any browser: the click is simply ignored, focus stays where it was, and the next Tab continues through the header as if the link had never been there. Nothing logs, nothing throws, and the test suite has no opinion, because the link and its target live in different files and neither is wrong on its own.
The landmark is already there
The fix is small, which is the good news. Those public pages do render a proper landmark, just without the id:
$ curl -s https://cogniprep.app/games | grep -o '<main[^>]*>'
<main class="flex w-full flex-1 items-center justify-center px-4 pt-20 pb-12 md:px-6 lg:px-12">
There are 125 <main tags across the repository and exactly one carries the id. So for most routes this is one attribute. The reason it is not one edit is that each public page renders its own <main> rather than inheriting one, which is the same reason the id drifted in the first place: there is no single place where "the main region" is defined, so there is no single place to attach the contract the root layout is already advertising.
The home page has no main region at all
The most visited route in the product is the exception that is worse than the rule. Asked for its landmarks, it returns one:
[...document.querySelectorAll('header,nav,main,footer,aside')].map(e => e.tagName)
// ["FOOTER"]
No <main>, no <header>, no <nav>. The page is a sequence of <section> and <div> elements with a footer at the end, so a screen reader user navigating by landmark gets one destination, and it is the bottom of the page. The skip link cannot be fixed there by adding an id, because there is nothing to add it to.
On the hub pages the practical loss from the broken link is honestly small. There are three tab stops before the main region:
Skip to main content, CogniPrep, Sign Up
Skipping three stops is not the difference between usable and unusable. But a skip link is also a claim, and an automated audit reads the claim, not the header size. Ours fails every check that resolves the fragment, on every page a search engine can see, while passing on the four pages nobody audits.
Heading order, while I was in there
Same one-liner, different query, across three public pages:
const hs = [...document.querySelectorAll('h1,h2,h3,h4,h5,h6')].map(h => +h.tagName[1]);
hs.reduce((skips, n, i) => (i && n - hs[i-1] > 1 ? [...skips, `h${hs[i-1]}->h${n}`] : skips), []);
/pricing and a provider page both answer ["h2->h4"]: one section drops a level, which screen reader users experience as a missing rung on the outline. The home page has 124 headings in the order h1, h2, then 116 h3s, then two h2s, then four h4s. The 116 are card and list titles, legitimately a long run of peers. The two h2s arriving after them are sections that come later in the document and are correctly h2, so the sequence is valid even though it reads strangely. The h4s at the end are the ones that want to be h3s.
Exactly one h1 per page, which is the thing that usually goes wrong and here does not.
What this kind of bug is
Everything here is a contract with software that never reports back. A broken fragment, a missing landmark, a skipped heading level: no exception, no failing build, no error in Sentry, no change in any metric we watch. The only way it surfaces is if somebody runs the audit, or if a user who depends on it tells us, and the users who depend on it mostly do not file bug reports about a product they have already left.
The patch list is short and none of it is deployed yet: id="main-content" on the shared public <main>, a real <main> wrapping the home page sections, and the four h4s demoted to h3. Writing it down is the cheap half.
See it
Open cogniprep.app/games, press Tab once, and the skip link appears top left. Press Enter. Nothing moves. Press Tab again and you are on the logo, exactly where you would have been. Then run document.getElementById('main-content') in the console for null, and document.querySelector('main') for the element that should have had the id. On the home page try [...document.querySelectorAll('header,nav,main,footer')].map(e => e.tagName) and count the landmarks.
Top comments (0)