Shokunin · Products, experiments, and the work behind them.

Two blank pages that every existing gate passed

2026-09-07

Both sites rendered nothing under conditions no test covered, and both builds exited 0. What was missing was a gate that loads the built artifact.

Both sites were rendering nothing under conditions no test covered, and both builds exited 0. On the pseudonymous site, the per-site build transform in vite.config.ts emitted a stub for every record that site does not publish, and the stub failed content validation. parseAll() in src/lib/publishingContent.ts throws at module init, so React never mounted on any route. Every page served its prerendered static shell with no stylesheet, no navigation and no app. A second cause sat inside the same throw: a record renamed for the pseudonymous site through shokuninSlug kept its updates pointing at the old slug, which failed the relation check.

Why nothing caught it is the part worth keeping. The type checker, 273 unit tests, the prerender parity tests and the output isolation gate all passed. scripts/vite-prerender.mjs reads the content files directly rather than through the bundle, so the static shells were correct while the client bundle was broken, and nothing in the repo ever loaded the built artifact. The fix makes the withheld stub valid while carrying no professional title, description or body, and keeps it marked draft so it stays out of every index, feed and sitemap.

The second failure was unrelated in cause and identical in symptom. src/three/SiteCanvas.tsx mounts a full-viewport WebGL canvas on every non-immersive route. A failed context threw past React and unmounted the whole app, so four top-level routes rendered blank. Detail routes survived by accident: a modal covers the canvas, so it never mounted there. Hardware acceleration turned off, an old integrated GPU, a locked-down corporate build and the browser's own GPU blocklist all produce this. The layer is decorative, and it now fails to null.

scripts/check-renders.mjs is the gate for this class. It serves the built output over HTTP, drives headless Chrome over the DevTools protocol, loads every route of both sites, and asserts the app actually mounted rather than that something rendered. The floor is 60 characters of text, set deliberately below the shortest real page, an update stub at 99 characters. Routes are discovered by walking the built output, so a new route is covered the moment it is prerendered. 192 routes pass. Reverting the stub fix drops the pseudonymous site to 0 of 13. A --no-gpu flag covers the WebGL case and replaced an older single-purpose script.