SEO Crawlability Playbook
A pure client-side-rendered (CSR) React + Vite SPA serves an empty<div id="root"></div>shell for every URL. All content and per-route meta tags are injected by JavaScript after load. Google can render JS in a deferred second wave, but non-JS crawlers (Bing, and social/link-preview bots like WhatsApp, LinkedIn, Facebook, X and Slack) see only the generic shell, so every shared link shows the same title, description and image. This playbook diagnoses the problem and fixes it with build-time prerendering.
Paste into your AI chat
Copy the entire playbook as markdown to use as context in Cursor, Claude, ChatGPT, or any AI tool.
The empty-shell problem
Why a client-only render hurts indexing and link previews.
Slower, less reliable indexing
Failed JS renders get indexed as the generic shell.
Duplicate titles/snippets
Every page can end up with the same title and description.
Non-JS crawlers see nothing
Bing is weak; link-preview bots run no JavaScript at all.
Next.js is usually fine
App Router SSR/SSGs by default; run the hygiene check instead.
Phase 0: Diagnose
Run these curl checks (~2 minutes) to confirm definitively whether the SPA serves an empty shell across routes.
Phase 1A: Vite build-time prerendering
After vite build, render every public route to dist/<route>/index.html with real content and correct head tags baked in. Replicate this proven reference implementation. Do not invent a new mechanism.
Env-var gotcha (Vercel / Supabase)
The build passes locally but fails on Vercel during prerender with something like "Supabase URL is not defined" thrown from a client/config module. Expect this on any Vite + Supabase site.
Phase 1B: Next.js hygiene check
Next.js App Router SSR/SSGs by default, so the empty-shell problem normally does NOT exist. This is a hygiene check, not a rebuild. Verify and fix only what is missing.
Phase 2: Shared SEO hygiene (both stacks)
Housekeeping that applies whether you prerendered a Vite SPA or verified a Next.js app.
Phase 3: Verify (evidence before claiming done)
Prove the fix worked. Evidence before claiming done.
Reference file layout
Where each concern lives in the Vite prerendering setup.
| Concern | File |
|---|---|
| Post-build renderer | scripts/prerender.mjs |
| Route manifest (source of truth) | src/prerender/routes.tsx + src/prerender/staticRoutes.json |
| Render + template injection | src/prerender/render.tsx |
| Sitemap generator | scripts/generate-sitemap.js |
| Env-aware robots.txt copy | scripts/copy-robots.js |
| Build wiring | package.json → "build" script |
| Tests | src/prerender/{routes,render}.test.tsx |
Want the full playbook as context for your AI coding assistant?