A React app rendered to static HTML at build time
Crawlers that never run JavaScript, and phones on slow networks, get the content in the first byte; the browser then hydrates the HTML instead of rendering from scratch.
Context
The site started as five static blocks, where plain HTML would have been enough. It now has an interactive lab (canvas simulations, live charts, server-sent events) and will get an owner-only admin panel. Links to individual experiments must unfurl properly when a recruiter pastes them into Slack or LinkedIn.
The first version shipped an empty <div id="root"> and rendered everything in the browser. That is
invisible to most AI crawlers: GPTBot, ClaudeBot and PerplexityBot fetch the raw HTML and do not run
JavaScript, and recruiters increasingly research candidates through exactly those tools. It also left
phones staring at a blank page until the bundle had downloaded and run.
Decision
React 19 + Vite + TypeScript with client-side routing, rendered to static HTML at build time:
vite build --ssr src/entry-server.tsx produces a renderer that web/scripts/prerender.mjs runs for every
route, writing one HTML file per route with its own title, description, canonical URL, Open Graph tags and
schema.org data (ProfilePage + Person on the home page). The browser hydrates that HTML instead of
rendering from scratch. Each experiment is a lazily loaded chunk that starts only after hydration; its
written notes are plain components imported with the page, so every prerendered lab page carries its framing
and the full explanation while the canvases and workers stay in the browser.
Consequences
- No server to run for HTML: the bucket serves files, and the first byte already carries the content.
- Anything browser-only (canvas, workers, scroll restoration, theme-dependent rendering) must stay out of
the server render;
useHydrated()gates it without a hydration mismatch. - The six experiments' notes ship in the main bundle (about 7 KB gzipped) so that they can be prerendered; the lab placeholder reserves most of a screen, so the notes move below the fold when an experiment loads and never count as a layout shift.
- The prerendered HTML must satisfy the strict Content-Security-Policy: no inline styles or scripts, so React Router's hydration data script is turned off (there are no loaders to hydrate).