How it works
The build pipeline.
prestruct adds two Node scripts to your existing Vite build. They run aftervite build, take about 2 seconds, and leave you with a dist/ that search engines can actually crawl.
Step by step
vite build
Your standard Vite production build. Produces dist/ with content-hashed JS and CSS bundles. dist/index.html is a shell -- meta tags are empty placeholders at this point.
node scripts/inject-brand.js
Reads your ssr.config.js and writes global title, meta description, Open Graph tags, Twitter Card tags, and JSON-LD schema into dist/index.html. This becomes the shell that step 3 builds per-route HTML on top of.
node scripts/prerender.js
Spins up a Vite dev server, loads your AppLayout via ssrLoadModule, wraps it in StaticRouter for each route, renders to string, stamps per-route title/description/canonical/og:url, and writes dist/route/index.html. Also generates 404.html with a real HTTP 404 status and a fresh sitemap.xml with today's date.
Cloudflare Pages deploy
CF uploads only changed files. Each route's HTML is served with HTTP 200 and Cache-Control: no-cache -- always fresh. Hashed JS/CSS assets get max-age=31536000, immutable. 404.html is served automatically with HTTP 404 for unmatched paths.
Caching strategy
Cache-Control: no-cache
Every HTML file revalidates on each request. Users always get the latest deploy. Since the actual content is on Cloudflare's CDN, the revalidation is fast and cheap.
Immutable, 1 year
Vite content-hashes every bundle filename. The hash changes when content changes -- so index-DnaYLP7Z.js will never change. Safe to cache forever.
Short TTL
sitemap.xml and robots.txt cache for 24 hours. Long enough to avoid hammering the origin, short enough to pick up route changes quickly after a deploy.
Key technical decisions
A compiled SSR bundle creates a separate module instance from the client bundle. StaticRouter and Routes end up with different copies of react-router-dom -- location context never propagates, every route silently renders as the homepage. ssrLoadModule uses Vite's unified registry: one instance, one context, correct output.
ssrLoadModule executes all imports at load time. BrowserRouter initializes immediately against
window.location, which defaults to / in Node. This fires before StaticRouter can set the correct location. BrowserRouter lives only in App.jsx. AppLayout only uses Routes, Route, useLocation.createRoot replaces the entire DOM on mount, causing a repaint even when the SSR HTML matches perfectly -- users see a flash (FOUC). hydrateRoot attaches React to the existing SSR DOM without touching it. The page that the crawler indexed is identical to what the user's browser paints. No flash, no mismatch.