Skip to content

fix(docs): sort the sitemap's entries so the route is a function of the content - #200

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-194-sort-sitemap-entries
Aug 26, 2026
Merged

fix(docs): sort the sitemap's entries so the route is a function of the content#200
hotlong merged 1 commit into
mainfrom
claude/issue-194-sort-sitemap-entries

Conversation

@hotlong

@hotlong hotlong commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Fixes #194

Two consecutive builds of an identical, untouched tree emitted different bytes for the same 346 sitemap entries. source.getPages() enumerates in file-read completion order, so the docs entries moved between builds in contiguous chunks while the advertised set stayed exactly the same. Sorting by url before returning makes the route a pure function of the content.

One statement changed. The sitemap builder is not reorganised, the staticPaths / docPaths composition is untouched, and the ordering is plain lexicographic by url rather than anything semantically meaningful.

Implemented against the corrected reason, not the card's headline

The card's stated urgency was that the locale-surface gate from #175 would flake if it compared raw bytes or asserted positionally. The grading comment already struck that framing, and I confirmed it independently against the landed gate rather than taking it on trust. .github/scripts/check-locale-surface.mjs reads every advertised url into a Map of url to count and compares it against a Set; its two .sort() calls stabilise the printed finding list rather than feeding any comparison. Order-invariant by construction, so entry order cannot flake it.

What does hold, and is enough on its own: the generated sitemap was not a pure function of the repository. deploy-docs.yml publishes on every push to main, so the served sitemap.xml changed bytes on deploys that changed no content, and anything downstream treating "bytes changed" as "content changed" was reading noise.

Why a code-unit comparison and not localeCompare

localeCompare is sensitive to the runtime's ICU data and default locale. A comparator whose result depends on the machine would reintroduce exactly the irreproducibility this change removes, just across machines instead of across builds — and it would do so invisibly, since any single machine would look perfectly stable. All 346 urls are distinct, so the comparison is a total order on the real data.

Verification — the card's own reproduction, inverted

cd apps/docs && pnpm run build && sha256sum .next/server/app/sitemap.xml.body, twice, on a tree left untouched between the two runs:

build tree raw sha256 sorted url blocks
pre-fix 1 b62c99f, untouched 60a0416c… a57adad5…
pre-fix 2 b62c99f, rebuilt, untouched f261b188… a57adad5…
post-fix 1 41d145d 84ec138c… a57adad5…
post-fix 2 41d145d, rebuilt, untouched 84ec138c… a57adad5…

The two pre-fix builds differ; the two post-fix builds are byte-identical. That is the check the card could not make, and it only becomes available once this lands.

The advertised set is unchanged. The sorted-url-blocks digest holds at a57adad5… across all four builds — the same digest the card recorded on both of its trees. Its normalisation is identified rather than guessed: sorted url blocks joined by newline reproduces a57adad5… exactly, so this is the card's own invariant and not a similar-looking one of mine. Entry count holds at 346, all distinct.

The locale-surface gate still passes, for the same reason as before. Green on the final tree: 346 advertised, 346 distinct, 346 expected, 0 unexpected, 0 missing, 0 duplicated. Stronger than re-running it: I ran the gate against the pre-fix artifact and the post-fix artifact and its output is byte-identical between the two, which measures the order-invariance claim above rather than asserting it. The pre-fix artifact was swapped in over the build output and restored under a trap, with the restore confirmed by sha256.

Other gates, all on 41d145d: pnpm run type-check clean; pnpm turbo run test --force green with Cached: 0 cached (the unforced run was a cache replay, which the test task's declared inputs make legitimate, but a replayed green is not a measurement); check-node-floor.mjs --self-test green. translations.yml is path-filtered and this diff matches none of its paths.

Scope

apps/docs/app/sitemap.ts only. lib/seo.ts is imported but not written, so there is no intersection with #178 — which landed as #198 while this was in flight, touching the two llms routes and robots.ts but not seo.ts. This branch was rebased onto it and every digest above was re-measured on the rebased tree. No changeset, no packages/.

One note for the reviewer: the diff is 11 lines, of which 10 are the explanatory comment. The adjudication asked for one line and that is what the code is; trim the comment freely if it reads as too much for the change.


Generated by Claude Code

…he content

The generated sitemap was not reproducible: two consecutive builds of an
identical, untouched tree emitted different bytes for the same 346 entries.
`source.getPages()` enumerates in file-read completion order, so the docs
entries moved between builds in contiguous chunks while the advertised set
stayed exactly the same.

Nothing user-visible was wrong — entry order carries no meaning to a crawler.
The cost is that an artifact meant to describe the content instead described
the build: `deploy-docs.yml` publishes on every push to `main`, so the served
sitemap.xml changed bytes on deploys that changed no content, and anything
downstream reading "bytes changed" as "content changed" was reading noise.

Sorting by url before returning makes the route a pure function of the content.
Compared by code unit rather than with `localeCompare`, whose result depends on
the runtime's ICU data and default locale — a machine-dependent comparator
would reintroduce the irreproducibility this removes, just across machines
instead of across builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5zjYc2BoFV2NjKBBapC7C
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.

[finding] the generated sitemap's entry order is not reproducible — two builds of an unchanged tree emit different bytes

2 participants