Dev.to WebDev 🛠 Dev 👁 0 📖 6 min read

Our public header went from one link to eleven, and six of them are not in the HTML until you click

Until this week the header on every public page of CogniPrep was this, and I had written it myself, and I had looked at it most days for months without seeing it: <header className="bg-background fixed top-0 right-0 l

Until this week the header on every public page of CogniPrep
was this, and I had written it myself, and I had looked at it most days for
months without seeing it:

<header className="bg-background fixed top-0 right-0 left-0 z-50 w-full">
  <div className="border-border/40 mx-[10px] border-b md:mx-[30px]">
    <div className="container flex h-14 max-w-screen-2xl items-center px-6 md:px-4">
      <BrandLockup href="/" />
      <nav className="ml-auto flex items-center gap-2 sm:gap-3">
        <Button asChild size="sm"><Link href="/signup">Sign Up</Link></Button>
      </nav>
    </div>
  </div>
</header>

One wordmark, one button, and a <nav> element whose only child was the button.
73 route files under app/ render that component, plus six article shells that
wrap the content clusters, so the number of public pages where the top of the
window offered no way to reach another section of the site was all of them.

Here is what replaced it, and what the replacement actually puts in front of a
reader, a crawler and a phone, which turn out to be three different answers.

Eleven destinations in two arrays

The whole navigation is two arrays and a predicate:

/** Shown as links on desktop and first in the mobile selector. */
const PRIMARY_LINKS: NavLink[] = [
  { href: '/games', label: 'Providers', icon: GameboyIcon },
  { href: '/tests', label: 'Test Types', icon: CheckListIcon },
  { href: '/interview', label: 'Interviews', icon: VideoReplayIcon },
  { href: '/assessment-centre-practice', label: 'Assessment Centre', icon: PresentationBarChart01Icon },
  { href: '/pricing', label: 'Pricing', icon: Tag01Icon },
];

/** Behind "More" on desktop and below the divider in the mobile selector. */
const MORE_LINKS: NavLink[] = [
  { href: '/employers', label: 'Employers', icon: Building03Icon },
  { href: '/blogs', label: 'Blogs', icon: News01Icon },
  { href: '/comparison', label: 'Compare Platforms', icon: BalanceScaleIcon },
  { href: '/cheating', label: 'Can You Cheat?', icon: Alert02Icon },
  { href: '/about', label: 'About', icon: InformationCircleIcon },
  { href: '/help', label: 'Help', icon: HelpCircleIcon },
];

/** A section is active on its hub and on every page beneath it. */
function isActive(pathname: string, href: string) {
  return pathname === href || pathname.startsWith(`${href}/`);
}

Five in the bar from lg, six behind a More dropdown, and below lg a single
selector holding all eleven with a divider where the arrays join. The split is
not a taxonomy. It is how many 14px labels fit next to a wordmark and a Sign Up
button before the bar wraps.

Five of the eleven are in the server HTML. Six are not

The More menu is a Radix dropdown, and a closed Radix dropdown renders nothing
at all, so its six items exist in the client bundle and in no markup. You can
count this from the outside. On cogniprep.app/pricing:

curl -s https://cogniprep.app/pricing | grep -o 'href="/pricing"' | wc -l      # 2
curl -s https://cogniprep.app/pricing | grep -o 'href="/comparison"' | wc -l    # 1

Every primary link appears twice, once in the header and once in the footer.
Every More link appears once, and that once is the footer. The same is true of
the mobile selector, so below lg the header contributes zero anchors to the
document: the only <nav> on the page is

<nav aria-label="Main" class="ml-5 hidden items-center gap-1 lg:flex">

which is display: none until 1024px. A phone gets a button labelled with the
current section and a landmark that is in the HTML but not on the screen.

None of that is a disaster, because the footer on those same pages is a real
site map with four columns, every test type and five named employers in it. But
it does mean the header was not the internal-linking change it looked like. The
five primary links are the change; the six behind More are a convenience for
people who are already here with a mouse.

The active section is a prefix match, and the landing page matches nothing

isActive treats a section as current on its hub and on everything beneath it,
so a provider page lights up Providers without any page having to say so.
Verify it on a hub child:

curl -s https://cogniprep.app/games/shl | grep -o 'aria-current="page"' | wc -l   # 2

Two, because the header marks Providers and the breadcrumb marks SHL. The
header's one looks like this in the live HTML, right after the wordmark:

<nav aria-label="Main" ...><a aria-current="page" class="rounded-full px-3 py-1.5 ...

The landing page is the interesting case, because / is in neither array. The
find returns undefined, and the mobile trigger falls back to a generic label:

curl -s https://cogniprep.app/ | grep -o '>Menu<' | wc -l    # 1

That is the one page where the selector cannot name where you are, and it is
also the one page where that is fine.

One component, two headers

The landing page needs the bar to sit over a hero image rather than on a solid
background, which is one prop:

overlay
  ? 'relative bg-black/20 backdrop-blur-sm'
  : 'bg-background fixed top-0 right-0 left-0'

Both variants are visible from a terminal. Grep the opening tag on the landing
page and on any other public page:

curl -s https://cogniprep.app/ | grep -o '<header class="[^"]*"'
# <header class="z-50 w-full relative bg-black/20 backdrop-blur-sm">

curl -s https://cogniprep.app/games/shl | grep -o '<header class="[^"]*"'
# <header class="z-50 w-full bg-background fixed top-0 right-0 left-0">

relative versus fixed is the whole difference, and it is the reason the
landing page needs no clearance while every other page does.

The height is in one file and the clearance is in sixty

h-14 is 56px, and it is stated once. What it costs is stated everywhere else.
A grep for top padding on the content wrapper of public pages finds pt-20 in
61 files, pt-24 in 18 places, and the pricing page going its own way with
pt-24 md:pt-28 lg:pt-32:

<main className="flex w-full flex-1 items-center justify-center px-4 pt-24 pb-12 md:px-6 md:pt-28 lg:px-12 lg:pt-32">

So a 56px bar is cleared by 80px on most pages, by 96 to 128px on
the pricing page, and the error fallback picks
between two values depending on whether it drew a header at all:

withPublicHeader ? 'pt-24' : 'pt-16'

Nothing is broken. It is just that position: fixed turns one number in one
component into a number every page has to know, and nothing in the type system
will tell you when one of them drifts. The next time I touch the bar height I
will have 80 files to grep, which is the honest price of the five links I wanted.

The same destination, four different names

One more thing fell out of putting the two surfaces side by side. The header and
the footer link to the same hubs under different anchor text:

href Header Footer
/games Providers Assessment Providers
/tests Test Types All Test Types
/interview Interviews Interview Practice
/employers Employers All Employers
/comparison Compare Platforms Compare Platforms

Four of five differ. For a reader that is invisible. For anything counting
internal anchor text it is two votes with two wordings, which is not wrong, just
unplanned, and it is the kind of thing you only notice when you grep the page
rather than look at it.

See it for yourself: open cogniprep.app/games,
narrow the window past 1024px and watch the five links collapse into one
selector that names the section you are in, then run the two grep -c commands
above and see how much of that the HTML admits to.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.