Why we hand-rolled the build script
The original plan reached for Astro. It's a good default for a docs site โ content collections, an island model, zero-JS-by-default pages. The reason we didn't ship it comes down to one reported bug.
The sidebar-flicker report
Cloudflare's own developer docs run on Astro + Starlight, and hit a filed, reproduced defect: the sidebar drops and the TOC highlight lags by a frame on every navigation, specifically on Cloudflare Pages + Chrome. It shipped as <ClientRouter />-driven view transitions โ fast, but with a failure mode nobody could fully explain.
We didn't want to inherit a bug we couldn't debug ourselves. A framework's client router is opaque by design; when it misbehaves, you're waiting on someone else's fix.
The fix that isn't framework-specific
Full static navigation โ every link a real page load, no client router โ sidesteps the whole class of bug. That's what a hand-rolled build does by default, since there's no router to misconfigure in the first place.
The tradeoff is real: no island hydration, no client-side transitions, no framework ecosystem to lean on. What we get back is a build we can read start to finish, and a navigation model with exactly one failure mode โ a broken link โ instead of an open-ended one.
That trade is the whole thesis behind build.mjs: markdown in, static HTML out, nothing in between that we didn't write ourselves.