Skip to content

docs: settle the sidebar label casing for the two auth pages - #1295

Merged
vivek7405 merged 15 commits into
mainfrom
docs/sidebar-label-casing
Aug 6, 2026
Merged

docs: settle the sidebar label casing for the two auth pages#1295
vivek7405 merged 15 commits into
mainfrom
docs/sidebar-label-casing

Conversation

@vivek7405

@vivek7405 vivek7405 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Closes #1259

The docs sidebar was Title Case in 42 of its 44 entries and in all 6 section titles. The two auth pages were the only exceptions, and each contradicted itself as well: its metadata.title was already Title Case while its nav label and <h1> were sentence case, so one page carried both conventions. This title-cases the pair, moves the seven inbound anchors that name them, and writes the convention down with a guard test so it cannot drift a third time.

Why Title Case

Two independent lines of evidence, both measured.

Internal. Of the 44 entries, 24 multi-word ones are unambiguously Title Case, 16 are single words and casing-neutral, Migrating from Next.js reads the same either way, @webjsdev/ui (AI-first) is a package name, and the remaining 2 are this pair. All 6 section titles are Title Case. So the scope is those two entries, not a list-wide normalization, and Title Case is also the far cheaper direction (2 edits against 26 labels plus 6 section titles).

External. Across 11 framework docs sites, Title Case leads roughly 8 to 2. Title Case: Next.js (88% of multi-word nav_title values), Rails (100% of 83), Vite, Remix v2, Remix v3, Nuxt, TanStack, Bun, shadcn. Sentence case: Tailwind (100% of 196) and Svelte. The split is not arbitrary: Title Case clusters in guide-heavy sidebars, sentence case in reference-heavy ones that are mostly literal identifiers (Tailwind's is 185 CSS property names out of 196). WebJs's sidebar is guide-heavy prose. Qwik, which never wrote a rule down, ships both API Reference and API reference in one sidebar, which is why this adds a guard rather than only fixing the strings.

All 11 keep code identifiers verbatim, with zero counterexamples. Next.js's nav_title key exists partly to UNDO Title Case and restore one (turbopackFileSystemCache, webpack, 'use client'). Hence Auth Providers (createAuth) and not CreateAuth.

What changed

  • app/docs/layout.ts, the two labels.
  • app/docs/{auth,authentication}/page.ts, the two <h1> values plus the cross-link each carries for the other. No metadata.title value moves: both chosen strings already ARE the existing title minus the | WebJs suffix.
  • app/docs/{cache,sessions,security,typescript,rate-limiting}/page.ts, the five inbound anchors that name one of the pages. Nothing tests anchor text, so these were found and verified by grep.
  • test/ssr/docs-links.test.ts, the new guard, plus corrections to two neighbouring comments (see below).
  • AGENTS.md, the convention.

Regression surface

Traced before implementing. NAV_SECTIONS is module-local and never exported; label reaches exactly one line, lib/ui/docs-shell.ts:307. lib/docs-llms.server.ts reads the layout for hrefs only and never captures label, page titles come from metadata.title, and the <h1> is stripped from the markdown body. So /llms.txt, /llms-full.txt, /docs/<topic>/llms.txt, /sitemap.xml, and the search index change zero bytes, which I confirmed by fetching /llms.txt rather than assuming. Docs pages emit no JSON-LD, there is no RSS route, and the docs OG title is a hardcoded constant. No redirect map or priority table keys off label text. Outside app/docs/, the two phrases appear nowhere in the repo.

The guard test

every docs sidebar label and section title is Title Case. Each choice is tied to a failure it prevents:

  • Slices to the NAV_SECTIONS literal first. Load-bearing, not defensive: outside it sit the metadata block's own title: keys and menuLabel: 'Documentation menu', which is legitimately sentence case and would be a false positive.
  • Reads each key on its own, in either quote style, and cross-checks labels.length === hrefs.length. An href:-anchored regex yields nothing for an entry written { label, href }, so a key reorder would silently drop it, and the floor alone is too coarse to notice, firing only once four entries are missing. A single-quote-only regex would likewise miss a double-quoted entry, and when both its keys are double-quoted the counts stay equal so the cross-check would not notice either. The quote character is captured and back-referenced instead, which closes that.
  • Normalizes wrapping punctuation before the exemption lookup, storing bare createAuth. The parenthesized form would red CI on a legitimate rename to (createAuth API).
  • Keeps the exemption as a named list (FIXED_CASING). It covers any word whose casing is fixed by something other than prose, meaning code tokens and brands that start lowercase (macOS, npm), since recasing either misspells it. Shape detection cannot carry this: a future webjs check label is two ordinary lowercase words. The friction is the feature: adding an entry records "this spelling is correct, not a slip".
  • A generous minor-word set, so a plausible Deploying on Railway does not red CI for a label that was never wrong.
  • Asserts each word starts with a capital rather than parsing true Title Case. A floor, deliberately: a stricter rule is one people delete the first time it fires on something legitimate.
  • Scoped to app/docs/layout.ts, never app/ui/layout.ts, whose labels are registry component names and would be 32 false positives.

Where the convention lives, and why

The review rounds kept finding false claims in the same paragraphs because the same facts were stated in both AGENTS.md and the test comment, with nothing keeping them in sync, so every fix had to land twice and the copies drifted apart again.

So AGENTS.md now carries only what cannot rot: labels are Title Case, identifiers keep their casing, a label need not match the page's h1 and the ones that differ are correct, the pin covers two named slugs and must not be generalised, /ui is exempt. Every count, example and taxonomy lives in the test comment, beside the code that measures it and in front of whoever would widen the pin. That entry shed most of its bulk, which matters in a tree block whose next-longest entry is 7 lines.

Two corrections to neighbouring pre-existing comments, both the same class of bug this PR is about (a fact repaired in one file and left stale in another):

  • The comment above a doc page h1 matches its sidebar label claimed the rest of the docs use a shorter nav label than their heading. Measured, 38 of the 43 doc entries are byte-equal, so it had the norm backwards. It also said both pages disagreed with their nav entries, when /docs/authentication was labelled Authentication and agreed with itself exactly.
  • A stale count of 45 doc pages in docs-links.test.ts (twice) and app/docs/layout.ts (twice). There are 43.

Test plan

  • node --test website/test/ssr/docs-links.test.ts, 8 tests (up from 7), all pass
  • Counterfactual: sentence-casing either label reds the new test, naming the entry and each offending word
  • Counterfactual: reverting either <h1> reds a doc page h1 matches its sidebar label, naming the slug
  • Counterfactual: reordering an entry's keys AND sentence-casing it is still caught, which an href:-anchored regex would have dropped silently
  • Counterfactual: a label shape the regex cannot read fails loudly (parsed 44 hrefs but 43 labels) instead of quietly checking one fewer
  • Guard verified non-vacuous: it parses all 44 labels and 6 section titles, and the tricky shapes (Runtime (Node & Bun), Migrating from Next.js, Editor Setup (Neovim, VS Code), @webjsdev/ui (AI-first)) are in the checked set
  • cd website && npm test, full suite green (443 server tests + 84 browser)
  • webjs check on website, all checks pass
  • grep for the sentence-case strings across rendered source returns nothing
  • /docs/auth and /docs/authentication render 200 with the correct <h1>
  • /llms.txt fetched: both entries already Title Case, zero bytes changed

Doc surfaces

  • Updated: website/AGENTS.md (the convention, pointing at test/ssr/docs-links.test.ts for the specifics), and the docs site itself, which is what this PR edits.
  • N/A because this changes copy in the docs app and ships no framework surface: root AGENTS.md, the skill at .agents/skills/webjs/, packages/*/README.md, the scaffold templates and generators, the MCP server, the editor plugins, website/app/page.ts marketing copy, and any changelog or version bump.
  • Browser, e2e, and Bun parity: N/A because nothing under packages/ moves, no runtime-sensitive surface is touched, and no client behaviour changes. The whole change is server-rendered strings the SSR suite covers. The four-app dogfood check is not triggered for the same reason, though website is one of those apps and the SSR suite boots it through createRequestHandler in prod mode and fetches every doc page. CI runs all of these regardless and was green.

@vivek7405 vivek7405 self-assigned this Aug 5, 2026

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read the whole diff: the two label renames, the seven inbound anchors, the new casing guard, and the AGENTS.md convention.

The rename half is right and the reasoning behind it holds up. Title Case is where 42 of the 44 entries already sat, metadata.title was already correct on both pages so no derived surface moves, and keeping createAuth lowercase inside the label is the call every comparable docs site makes.

The guard test is the part I'd keep an eye on, and it mostly earns its keep: reading label: unanchored with an href cross-check is the right instinct, because the obvious href-anchored version would silently stop checking any entry someone reordered. Two real problems though, and the worse one is in AGENTS.md rather than the code. The convention paragraph asserts a rule the docs do not follow and credits a test that does not enforce it, which is the kind of wrong that actively misleads an agent into "fixing" four correct pages. The other is the guard's own failure message contradicting the code path directly beneath it.

Comments inline.

Comment thread website/AGENTS.md Outdated
Comment thread website/AGENTS.md Outdated
Comment thread website/test/ssr/docs-links.test.ts Outdated

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second pass, scoped to the fix commit and what it touches.

The failure-message correction holds up. The printed word is the same value the exemption set is looked up with, so "spelled exactly as this message printed it" is now literally true, and the cache() to cache example is right.

The AGENTS.md rewrite did not. It swapped one false claim for another, which is worse than the original because the whole point of that commit was to stop the file asserting things the docs do not do. I measured the sidebar: 39 of the 43 doc entries already read exactly the same in the label and the h1, so "a nav label may be shorter, and usually is" inverts the norm. Both examples it gave were longer labels, not shorter ones, so they argued against their own sentence. The historical clause was wrong twice over as well.

The root cause is that I took the claim from the test's own comment instead of measuring, and that comment has been wrong since it was written. So it is fixed in both places now, which is the only way the convention and the test stop contradicting each other.

Comment thread website/AGENTS.md Outdated
Comment thread website/AGENTS.md Outdated
Comment thread website/test/ssr/docs-links.test.ts Outdated

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Third pass over the same paragraphs, and it found the subtlest one yet.

The direction of the claim was right this time, but the basis was not. The census I quoted (39 match, 4 diverge) is what you get after decoding HTML entities, while the pin it is reasoning about is byte-equal and implemented as a source-byte comparison. On that basis it is 38 and 5. The missing fifth is /docs/conventions, whose h1 escapes its ampersand where the label carries the raw character, so it reads identically and compares unequal. That is precisely the surprise waiting for anyone who widens this test, so it is worth naming rather than rounding away.

Second thing, and it has survived two rounds: the pin's stated rationale did not justify the pin. Two pages sharing an h1 and a metadata title is a duplicate-title problem, and there is already a separate test for that. What justifies THIS test is narrower and sharper, that /docs/auth rendered a heading byte-identical to its sibling's label, so clicking one nav entry landed you on a heading naming the other page.

Also corrected a stale count of 45 in two older comments, since the 43 introduced here would otherwise contradict it a few lines away in the same file.

Comments inline.

Comment thread website/AGENTS.md Outdated
Comment thread website/AGENTS.md Outdated
Comment thread website/test/ssr/docs-links.test.ts Outdated
Comment thread website/test/ssr/docs-links.test.ts

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fourth pass, and this one is the useful one because it stopped looking at the sentences and looked at why they keep being wrong.

Every number in the previous commit checks out. The problem was structural. The same prose lived in two files with nothing keeping them in sync, so each fix had to land twice, and the two copies had already drifted apart on the one piece of arithmetic they both restated. That is four rounds of the same failure, and no amount of further wording care would have stopped a fifth.

There was also a contradiction the byte-basis correction created rather than removed. Stating that 38 of 43 entries are byte-equal IS an audit of all 43, so the sentence two paragraphs later claiming the other 41 had never been audited could not also be true. Both halves only collided once they moved onto the same basis.

So the counts, the examples and the taxonomy are out of AGENTS.md entirely and live in the test comment, next to the code that measures them and in front of the reader who would widen the pin. What stays in the contract is the part that cannot rot. The entry drops from 38 lines to 18, which matters in a block whose next-longest entry is 7.

I dropped the shorter/longer/reworded taxonomy instead of repairing it, because it did not survive checking: the reworded example is shorter than its heading and the Task entry filed under longer shares only a prefix, so length was never the axis. And I answered a question none of the four rounds had: why /docs/authentication is pinned at all.

Comment thread website/AGENTS.md Outdated
Comment thread website/AGENTS.md
Comment thread website/test/ssr/docs-links.test.ts
Comment thread website/test/ssr/docs-links.test.ts

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fifth pass, over the commit that pulled the counts out of the contract file.

That restructure held up. Everything left in the test comment measures correctly, the history checks out, and the removal did not strand anything an agent needs.

Two things it missed, both the same shape as the bug this PR fixes: a fact stated in one place and left stale in its neighbour. AGENTS.md described the pin as guarding a collision between the two headings, when the test compares each page's heading to its own label and never looks at the other page, so the sentence promised a guarantee the test does not make. And the stale page count I corrected twice in the test file was still sitting in two comments in layout.ts, which this PR edits anyway. That one got worse when the counts moved, since AGENTS.md now sends the reader to the test for numbers and layout.ts was the last place carrying a wrong one.

Comments inline.

Comment thread website/AGENTS.md
Comment thread website/app/docs/layout.ts

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final read of the whole change rather than of any one commit.

The substance is right and I checked it rather than trusting the earlier passes: both metadata titles unchanged and already Title Case, all seven anchors moved with nothing missed repo-wide, the page counts, the five divergent slugs with conventions differing only by its ampersand escape, the #1103 history, and the /ui exemption.

Three things worth fixing, and the interesting one is that the guard's own comment and its failure message had each drifted from the code they sit next to.

The comment justified reading label: unanchored by saying a guard quietly checking 30 of 44 labels is the failure it prevents. The floor thirteen lines below demands more than 40, so 30 fails loudly and that scenario cannot happen. The real argument is narrower and still good: the floor only fires once four entries are missing, so the href cross-check is what makes a single dropped entry loud.

The failure message had no remedy for the one false-fire it will plausibly hit. A label like "Deploying on macOS" trips the capital check on a correctly spelled brand, and the message's first remedy, the one it calls the fix nearly every hit wants, tells the reader to recase it into MacOS. The exemption set already handles this mechanically, so I widened what it is documented to be for rather than adding anything.

One finding I am not taking: the report flagged the test-plan boxes as all unchecked. That was against a stale copy of the body, not the PR.

Comment thread website/test/ssr/docs-links.test.ts Outdated
Comment thread website/test/ssr/docs-links.test.ts Outdated

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check of the fix that widened the casing exemption to cover lowercase brands.

The mechanics were right. The floor arithmetic holds, and an exact-case lookup on the stripped token means adding macOS genuinely silences a label containing it. But the fix only reached the two places that DESCRIBE the exemption and missed both places that STATE the rule, so the test's header and the AGENTS.md line still said prose or code identifier, a dichotomy with no room for a brand. The enforcement permitted macOS while both statements of the convention forbade it, and AGENTS.md is the one an author actually follows.

The other half is a self-inflicted overclaim. I had written that the href cross-check makes even a single dropped entry loud. It does not: an entry written with double quotes on BOTH keys is invisible to both regexes at once, so the counts stay equal and the guard passes over it. I confirmed it by double-quoting one entry, which walked a sentence-case label straight past the guard. It is not a hole in the file, since the orphan test reds on that shape, but promising a guarantee the test does not make is exactly what the previous round pulled me up on, so the comment now names the gap and where it is really caught.

Comments inline.

Comment thread website/test/ssr/docs-links.test.ts
Comment thread website/test/ssr/docs-links.test.ts Outdated
Comment thread website/AGENTS.md

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second check of the fix chain, and it found the gap I had documented was documented WRONG.

I had written that a double-quoted entry is caught by the sibling orphan test. That is true for a /docs entry and false for the one /ui cross-link, which is not a doc directory. Double-quoting both keys of it leaves labels and hrefs equal at 43, above the floor, with no orphan reported, so nothing in the file would have caught a sentence-cased relabel of the Component Library link. I had replaced an overclaim with a narrower overclaim.

So this stops describing the hole and closes it. The quote character is captured and back-referenced rather than hard-coded, which reads both styles and lets a label carry the other quote inside it. Both shapes that used to escape now fail the guard by name.

Also renamed the exemption set. It was still called IDENTIFIERS after being widened to hold brands, and macOS is not an identifier in any sense, so the symbol disagreed with the rule it encodes. That was the last surviving piece of the pre-widening wording.

I am not calling this converged. This is the second consecutive check to find something, and by the cycle's own rule that is the point to stop rather than push for a clean pass. These fixes are on the branch unreviewed.

Comment thread website/test/ssr/docs-links.test.ts Outdated
Comment thread website/test/ssr/docs-links.test.ts Outdated

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read the quote-agnostic parse and the FIXED_CASING rename, with the surrounding file for context. Nothing wrong with either.

Scope note so the trail is honest: this read the tree at 13364976. While it was running I found and pushed one more thing on top, so this clean result does NOT cover a14e056b. That commit is looked at separately.

The thing I found there is worth recording, because widening the parse to both quote styles changed more than the quote character. The old pattern required at least one character and the replacement matched zero, so an empty label went from unmatched, which made the label and href counts diverge and fired the cross-check, to matched-as-empty, which kept the counts equal and then fell through the has-a-letter test in silence. An empty label is a sidebar row with no visible text. It is back to requiring a character, which keeps both quote styles and restores the loud failure.

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check of the commit that made the parse require a non-empty value.

The change itself is right and I confirmed it: with the plus, an empty label leaves the counts uneven and the entry gets caught; the star let it match as an empty string and vanish at the has-a-letter test. Both quote styles, a value carrying the other quote, and a one-character value all still parse, and no legitimate nav value is empty, so there is no false failure.

What was wrong is the sentence I wrote around it, for the fourth time in this PR. It promised that an empty value stays unmatched and that the count cross-check is what fires on it. That is true for one shape out of four. In the other key order the label pattern swallows the rest of the line as its value, so the counts stay even and the entry surfaces as a garbage row in the offender list instead. An empty href behaves the same way, and an empty section title has no cross-check at all, only a floor it still clears.

The helper serves three keys, so any single sentence about what happens to an empty value was going to be wrong for two of them. I have stopped writing that kind of sentence here. The comment now says what the pattern does and calls the cross-check a signal rather than a guarantee. The assertion message also pointed the reader at the parser rather than the nav entry, which aimed them straight back at the star this commit had just reverted.

The pattern across this PR is mine and worth naming: the code has been right throughout, and every round has found a comment claiming more than it can deliver. The fix that finally stuck was writing less, not writing more carefully.

Comment thread website/test/ssr/docs-links.test.ts
Comment thread website/test/ssr/docs-links.test.ts
Comment thread website/test/ssr/docs-links.test.ts

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final read of the whole change, and unlike the rounds before it this one found a hole in the GUARD rather than in the prose around it.

MINOR_WORDS is matched case-insensitively, so any entry in it shadows a fixed-casing word that lowercases to the same string, and the shadow wins because the minor-word branch runs on the lowercased token. 'vs' was in that list, so the VS of "Editor Setup (Neovim, VS Code)" never reached the capital check, and a drift to "vs Code" passed with zero offenders. That is the guard failing to guard, in the one label where a brand and a minor word collide. VS has moved to FIXED_CASING, 'vs' is out of MINOR_WORDS, and the list now carries a note so the next short word added gets checked against the labels first.

The parse also stopped at an escaped same-quote: "What's New" captured "What" and left the rest of the label unread, with no count mismatch and no offender, so the test passed green having checked almost none of it. The value now consumes an escape pair or any non-terminator.

Two smaller ones: the helper's parameter was an implicit any in a .ts file, which nothing catches because website/test sits outside the tsconfig include; and the PR body still quoted a line count from an earlier commit. I dropped the number rather than correcting it, since it had already rotted twice.

Worth recording what this run of reviews actually taught me. The code was right from the first commit and the churn was almost entirely my own claims about it, so the fixes that stuck were the ones that removed a claim rather than reworded it. This round is the exception that justifies the whole exercise: two of the four findings are real defects in shipped behaviour, and both were invisible to every test.

Comment thread website/test/ssr/docs-links.test.ts
Comment thread website/test/ssr/docs-links.test.ts
Comment thread website/test/ssr/docs-links.test.ts

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delta round on the VS and escaped-quote commit.

The mechanics hold: the drift direction works, no other label changed verdict when 'vs' left MINOR_WORDS, the new capture handles both quote styles, the other quote inside a value, an escaped same-quote and a one-character value, and it still refuses an empty value.

Two problems, and the second is the one that mattered. My comment stated the mechanism backwards: FIXED_CASING is checked BEFORE MINOR_WORDS, so a minor word cannot swallow a correctly-cased VS. What a colliding minor word actually does is permit the DRIFTED spelling, since FIXED_CASING is exact-case and never matches 'vs'. The old wording described the code as it was before the fix.

And the 'VS' entry was documentation rather than protection. It changes no verdict on its own, and re-adding 'vs' to MINOR_WORDS reopens the hole silently. That path is not hypothetical, because remedy 3 of this test's own failure message tells the next person to add a lowercase-in-title word to MINOR_WORDS. So the rule is asserted now and the reopen reds with a message naming the collision.

Separately I closed the gap I had flagged rather than leaving it: section titles had no cross-check, only a floor, so an empty one dropped the count from 6 to 5, cleared "> 3" and passed silently. Sections are now counted independently by their items: key, the way hrefs count entries independently of labels.

All three verified by counterfactual, run after committing so the toggle restores the fix rather than discarding it.

Comment thread website/test/ssr/docs-links.test.ts
Comment thread website/test/ssr/docs-links.test.ts
@vivek7405
vivek7405 marked this pull request as ready for review August 6, 2026 05:43
@vivek7405
vivek7405 force-pushed the docs/sidebar-label-casing branch from d5b3498 to 68ff1f5 Compare August 6, 2026 05:44
The docs sidebar is Title Case in 42 of its 44 entries and in all 6
section titles. These two were the only exceptions, and each page
contradicted itself as well: the metadata.title was already Title Case
while the nav label and the h1 were sentence case, so one page carried
both conventions.

#1103 retitled the pair so they stopped sharing a metadata.title. That
rename was the point and stays. The casing was never considered, and
this is the pass that settles it.

Title Case is where the docs already sit and where the comparable docs
sites sit. Next.js title-cases 88% of its multi-word nav labels and
Rails all 83 of its guide names, against Tailwind and Svelte, whose
sidebars are mostly literal identifiers and are sentence case
throughout. createAuth keeps its own casing inside the label, which
every framework surveyed does with an identifier and which is why the
label reads Auth Providers (createAuth) rather than CreateAuth.

No metadata.title value moves. Both chosen strings already ARE the
existing title minus the WebJs suffix, so the no-duplicate-title guard
and every derived surface (llms.txt, the sitemap, the search index)
are untouched. The seven inbound anchors that name these pages move
with them, since nothing tests anchor text.
Fixing the two strings does not stop the next page picking a casing by
coin flip, which is how they drifted in the first place. Qwik's sidebar
ships both "API Reference" and "API reference" for exactly that reason.
So the rule is written into AGENTS.md and enforced by a test.

The test is a floor, not a Title Case parser: it asserts each word
starts with a capital, so a stricter rule cannot fire on something
legitimate and get deleted. Three things it deliberately does NOT do.
It does not anchor `label:` to a preceding `href:`, because that regex
yields nothing for an entry written { label, href } and would silently
stop checking it, so the href count is the cross-check instead. It does
not detect identifiers structurally, because a future "webjs check"
label is two ordinary lowercase words and no shape rule can tell it
from the slip being hunted, so the exemption is a named list whose
friction is the point. It does not cover the /ui sidebar, whose labels
come from the live registry and would be 32 false positives.

The NAV_SECTIONS slice is load-bearing rather than defensive: the
metadata block below it has its own title: keys, and the shell call
carries menuLabel: 'Documentation menu', which is legitimately sentence
case and would otherwise be reported as an offender.
The convention paragraph claimed a doc page's h1 reads exactly as its
nav label, and named a test as enforcing it. Both halves were wrong.
Four pages contradict the rule today (Introduction over a "Getting
Started" heading, "Runtime (Node & Bun)" over "Runtime", "Task (Async
Data)" over "Task Controller", "Editor Setup (Neovim, VS Code)" over
"Editor Setup for VS Code & Neovim"), and the named test iterates
exactly two slugs, with a comment saying that scoping is deliberate
because the rest of the docs use a shorter nav label than their
heading.

AGENTS.md is the agent contract, so the false rule was the harmful
kind: an agent reading it would either rewrite those four headings or
recase the four labels, and the existing test comment says both are
wrong. It now says casing is the only thing pinned sidebar-wide, that
a nav label may be shorter than the h1, and that the byte-equality pin
is scoped to the two auth pages and must not be generalised.

Also fixes the guard's own failure message, which told the reader to
add an identifier VERBATIM while the lookup runs on the word AFTER
wrapping punctuation is stripped, so a literal 'cache()' entry never
matches and only 'cache' works. It now says to spell it exactly as the
message printed it, which is the stripped form. And drops Drizzle as
an example of the identifier rule, since it is Title Case anyway and
demonstrates nothing.
The previous pass replaced one false claim with another. It said a nav
label may be shorter than the page's h1 "and usually is", when the
measured norm is the opposite: 39 of the 43 doc entries read exactly
the same in both places. Both examples it offered were wrong too, since
"Runtime (Node & Bun)" over "Runtime" and "Task (Async Data)" over
"Task Controller" are LONGER labels, so they argued against the
sentence they illustrated. Only getting-started is genuinely shorter.

The historical clause was also wrong on both halves. It said the two
auth pages once carried headings matching neither their labels nor each
other. At befd1d8^ both rendered <h1>Authentication</h1>, so they
matched each other exactly, and /docs/authentication was labelled
'Authentication', so that one matched its own label. Only /docs/auth,
labelled 'Auth (Providers)', mismatched.

Now says what the numbers say: equality is the norm but not a rule, 4
entries diverge deliberately and must not be "fixed", and the pin
covers two slugs because the other 41 have never been audited against
byte-equality and 4 would fail it.

Corrects the same falsehood in the test's own comment, which is where
the previous pass took it from. Leaving it there would have shipped a
convention in AGENTS.md that the adjacent test comment contradicts.
The previous pass fixed the direction of the claim but counted on the
wrong basis. It said 39 of 43 entries read the same and 4 diverge,
which is the RENDERED census, then used it to reason about a pin that
is byte-equal and implemented as a source-byte comparison. On that
basis it is 38 and 5.

The fifth is conventions, whose h1 escapes its ampersand as an entity
while the label carries the raw character, so the two read identically
and compare unequal. That is exactly the trap someone widening this
test would hit, so both surfaces now name it.

Also sharpens the pin's rationale. Saying the two pages once shared an
h1 and a metadata title is true but does not justify a LABEL pin, and
the duplicate title is already guarded by its own test. The real reason
is narrower: /docs/auth rendered an h1 byte-identical to its SIBLING's
label, so a reader clicking one nav entry landed on a heading naming
the other page. The test comment said "the pair" disagreed with their
nav entries, when /docs/authentication was labelled 'Authentication'
and agreed with itself. Only one of the two ever mismatched.

Corrects a stale 45 in two older comments in the same file, which the
new 43 would otherwise contradict on the same page. The sidebar links
43 doc pages (44 href keys, one of which is /ui).
Four review rounds each found a false claim in the same paragraph, and
the last one named why: the prose lived in two files at once, with
nothing keeping them in sync, so every fix had to land twice and the
two copies drifted apart again. Round 4 caught them already disagreeing
on the one piece of arithmetic they both restated.

The measurements were also self-contradicting. Saying 38 of 43 entries
are byte-equal IS a byte-equality audit of all 43, so the next sentence
claiming the other 41 had never been audited could not both be true.
That collision appeared the moment both halves moved onto the byte
basis, which is what the previous round did.

So AGENTS.md keeps only what cannot rot: labels are Title Case,
identifiers keep their casing, a label need not match the h1 and the
ones that differ are correct, the pin covers two named slugs and must
not be generalised, /ui is exempt. Every count, every example, and
every taxonomy moves to the test comment, which sits beside the code
that measures them and is the thing a reader consults before widening
the pin. The docs/ entry goes from 38 lines to 18, back in proportion
to a block whose next-longest entry is 7.

Drops the shorter/longer/reworded taxonomy rather than repairing it.
It did not survive contact: Task (Async Data) over "Task Controller"
was filed as LONGER while sharing only a prefix, and the reworded
example is in fact shorter than its heading, so length was not the axis
separating the buckets.

Also states why /docs/authentication is pinned at all, which no round
had explained: the collision was between one page's h1 and the OTHER's
label, so pinning half of it leaves the other half free to drift back.
AGENTS.md said the auth pair is held byte-equal "after their headings
once collided", which describes a heading-versus-heading problem. The
test compares each page's h1 to its OWN label and never compares the
two headings, so a reader taking that at face value would think the pin
guards duplicate headings. It does not, and that guard is a separate
test on metadata titles. Now says each h1 is pinned to its own label,
after one page rendered a heading that named the other page.

Also corrects the stale "45 pages" in two layout.ts comments. There are
43. This PR already fixed the identical number twice in the test file
and edits layout.ts in the same breath, so leaving it here meant the
same wrong count was repaired in one file and left in its neighbour.
It matters more now that AGENTS.md delegates the counts to the test,
which leaves layout.ts as the last place an agent editing the sidebar
would read a false one.
The guard flags any word not starting with a capital, so a label like
"Deploying on macOS" fires on a correctly spelled brand. None of the
three remedies fit it. The first, which the message calls the fix
nearly every hit wants, yields MacOS and is actively wrong; the second
was scoped in its own text to packages, exports, commands and
filenames; the third would permit the word anywhere but the first
position, which is not the rule for a brand.

The exemption set already handles it mechanically, so this widens what
the set is FOR rather than adding machinery: words whose casing is
fixed by something other than prose, which is code tokens and brands
that start lowercase. Recasing either one misspells it.

Also corrects the comment above the label regex. It justified the
unanchored read by saying a guard quietly checking 30 of 44 labels is
the failure mode it prevents, but the floor beneath it demands more
than 40, so 30 would fail loudly and that scenario cannot occur. The
floor only fires once four entries are missing. The href cross-check
is what makes a single dropped entry loud, which is the real reason
it is there.
The previous commit taught the exemption set and the failure message
that a lowercase brand is legitimate, but left the two places that
state the RULE saying otherwise. The test's header still said prose
takes the convention and a code identifier is written verbatim, a
dichotomy with no room for macOS, and AGENTS.md still said a code
identifier keeps its casing, which is the sentence an author actually
follows. So the enforcement allowed a brand while both statements of
the rule forbade it. Both now say a word whose casing is fixed by
something other than prose keeps it, covering code tokens and
lowercase-initial brands alike.

Also walks back an overclaim introduced by that same commit. It said
the href cross-check makes even a single dropped entry loud, which is
false: an entry written with double quotes on BOTH keys is invisible to
both regexes at once, so the counts stay equal and this test passes
over it. Confirmed by double-quoting one entry, which slipped a
sentence-case label past the guard while reding 'every doc page on disk
is reachable from the sidebar' instead, since that test reads
single-quoted hrefs and reports the page as orphaned. The comment now
names the gap and where it is actually caught, rather than promising a
guarantee this test does not make.
The comment claimed a double-quoted entry is caught by the sibling
orphan test. That holds for a /docs entry but not for the one /ui
cross-link, which is not a doc directory, so nothing in the file would
have caught a sentence-cased relabel of it. Verified: double-quoting
both keys of the /ui entry left labels and hrefs equal at 43, above the
floor, with no orphan reported.

Rather than document a narrower version of the gap, this closes it. The
quote character is captured and back-referenced instead of hard-coded,
so both styles parse and a label may carry the other quote inside it.
Both shapes that previously escaped now fail the guard by name.

Renames IDENTIFIERS to FIXED_CASING. The set was widened to hold brands
that start lowercase, and macOS is not an identifier in any sense, so
the symbol an author greps for disagreed with the rule it encodes. That
was the last place the pre-widening wording survived.
Widening the parse to both quote styles quietly swapped `[^']+` for
`.*?`, and the difference is not just the quote character: the old
pattern required at least one character while the new one matched zero.
So `label: ''` went from unmatched, which made the label and href counts
diverge and fired the cross-check, to matched-as-empty, which kept the
counts equal and then fell through the has-a-letter test in silence.

An empty label is a sidebar row with no visible text, so losing that
signal is a real loss even though nothing in the tree has one today.
Back to `+`, which keeps both quote styles and restores the loud
failure. Verified: an empty label now reports 44 hrefs against 43
labels.
The comment said an empty value stays unmatched and that this is what
makes the count cross-check fire. That is true for one shape out of
four. Measured: an empty label in { href, label } order does diverge the
counts, but in { label, href } order the label pattern swallows
"', href: " as its value, so the counts stay equal at 44 and the entry
surfaces as a garbage row in the offender list instead. An empty href
keeps the counts equal for the same reason, and an empty section title
takes titles from 6 to 5, which still clears its floor.

The helper serves three keys, so a sentence about what happens to "an
empty value" was always going to be wrong for two of them. It now
states what the pattern DOES and says plainly that the cross-check is a
signal rather than a guarantee, since a malformed entry can surface
through the offender list instead, which is equally loud.

The cross-check message also diagnosed the parser rather than the nav,
telling the reader the test cannot read the entry, which points at
widening the regex, back to the `*` the previous commit reverted. It
now names an empty value as a usual cause and says to fix the entry
first, widening only if the entry is legitimate.

Both empty-label orderings verified to fail loudly, by different
assertions, which is what the comment now claims and no more.
MINOR_WORDS is matched case-insensitively, so an entry in it shadows
any fixed-casing word that lowercases to the same string, and the
shadow wins because the minor-word check runs on the lowercased token.
'vs' was in the list, so the VS of 'Editor Setup (Neovim, VS Code)'
never reached the capital check at all, and a drift to 'vs Code' passed
with zero offenders. Verified both ways: the drift now fails naming
'vs', and the real label still passes.

VS moves to FIXED_CASING, where it belongs, and 'vs' comes out of
MINOR_WORDS. Nothing uses it as a minor word today, and a future
'WebJs vs Next.js' will red once with a message that says where to put
it. The list now carries a note about the shadowing, so the next person
adding a short word checks first.

Also makes the parse read an escaped same-quote. `(.+?)` with a
back-reference stopped at the backslash in 'What\'s New' and captured
'What\', leaving the rest of the label unchecked with no count mismatch
and no offender, so it passed green while reading almost none of it.
The value now consumes an escape pair or any non-terminator, verified
against a label written that way.

And annotates the helper's parameter, which was an implicit any in a
.ts file. website/test is outside the tsconfig include, so nothing
would have caught it.
Three things, all on the guard rather than around it.

The comment I added with the VS fix stated the mechanism backwards.
FIXED_CASING is checked before MINOR_WORDS, so a minor word cannot
swallow a correctly-cased VS; what it actually does is permit the
DRIFTED spelling, because FIXED_CASING is exact-case and never matches
'vs'. The old wording described the pre-fix code and told a reader the
two checks run in the opposite order from the source.

Worse, the 'VS' entry was documentation, not protection. It changes no
verdict on its own, and if someone re-adds 'vs' to MINOR_WORDS the hole
reopens silently. That path is not hypothetical: remedy 3 of this
test's own failure message tells the next person to add a
lowercase-in-title word to MINOR_WORDS. So the rule is now asserted,
and re-adding 'vs' reds with a message naming the collision.

Adds the cross-check section titles never had. Labels are counted
against hrefs, but titles had only a floor, so an empty section title
dropped the count from 6 to 5, cleared "> 3", and passed in silence.
Sections are counted independently by their items: key, the way hrefs
count entries independently of labels.
@vivek7405
vivek7405 force-pushed the docs/sidebar-label-casing branch from 68ff1f5 to 9d9bfde Compare August 6, 2026 05:51
@vivek7405
vivek7405 merged commit 7807263 into main Aug 6, 2026
10 checks passed
@vivek7405
vivek7405 deleted the docs/sidebar-label-casing branch August 6, 2026 05:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs: settle the sidebar label casing for the two auth pages

1 participant