ooxml.dev gets ~270 sessions/quarter with only 40 from organic search. But organic visitors have excellent engagement (3+ min avg, 12% bounce) — proving the content is valuable. The problem is zero /docs/* pages get organic traffic because Google can't index a client-side SPA. All doc content is static (defined in docs.ts), making pre-rendering straightforward.
Use react-dom/server's renderToString in a custom build script to generate static HTML for each route. No framework migration, no new runtime dependencies.
Runs after vite build:
- Reads
dist/index.htmlas template (has hashed CSS/JS from Vite) - For each route, renders the page component to HTML via
renderToString - Injects per-page
<title>,<meta>, OG tags, canonical URL, JSON-LD into<head> - Writes to correct path (e.g.,
dist/docs/tables/index.html) - Client-side React still loads and takes over for interactivity
Routes to pre-render:
/(Home)/mcp(MCP page)/spec(shell only — content is API-dependent)/docs+ all 7 doc pages fromdocs.ts
SuperDocPreview handling: Add typeof window === 'undefined' guard to render the XML as a static <pre><code> block during SSR. React hydrates the interactive version client-side.
Per-route metadata:
<title>— e.g., "OOXML Tables (w:tbl) — Structure & Implementation | ooxml.dev"<meta name="description">— fromdocs.tsdescriptions<link rel="canonical">—https://ooxml.dev{path}- Open Graph tags (og:title, og:description, og:url, og:type)
- Twitter card meta
For doc pages, auto-generate from docs.ts (title, badge, description).
Simple useEffect hook so browser tab title updates during SPA navigation. Used in DocsPage, Home, Mcp, SpecExplorer.
Injected by prerender script per page:
- Doc pages:
TechArticleschema - Home:
WebSiteschema withSearchActionfor/spec?q=
Generated by prerender script → dist/sitemap.xml:
- All routes with
<loc>,<changefreq>,<priority> - Auto-includes new doc pages from
docs.ts
User-agent: *
Allow: /
Sitemap: https://ooxml.dev/sitemap.xml
Plus existing AI crawler blocks.
- "build": "tsc && vite build"
+ "build": "tsc && vite build && bun scripts/prerender.tsx"| File | Action |
|---|---|
apps/web/scripts/prerender.tsx |
Create — core prerender + sitemap generation |
apps/web/src/data/seo.ts |
Create — per-route SEO metadata |
apps/web/src/hooks/useDocumentTitle.ts |
Create — client-side title hook |
apps/web/src/components/SuperDocPreview.tsx |
Modify — add SSR fallback |
apps/web/src/pages/docs/Page.tsx |
Modify — use useDocumentTitle |
apps/web/src/pages/Home.tsx |
Modify — use useDocumentTitle |
apps/web/src/pages/Mcp.tsx |
Modify — use useDocumentTitle |
apps/web/src/pages/SpecExplorer.tsx |
Modify — use useDocumentTitle |
apps/web/public/robots.txt |
Create |
apps/web/package.json |
Modify — update build script |
bun run buildfromapps/web/- Inspect
dist/docs/tables/index.html— should contain full HTML content, correct<title>, meta tags, JSON-LD - Inspect
dist/sitemap.xml— should list all routes - Serve
dist/locally (bunx serve dist) and verify:- Pages load with correct content before JS executes (disable JS in browser)
- Interactive features (SuperDocPreview, spec search) work after JS loads
- SPA navigation updates browser tab title
- Deploy and submit sitemap to Google Search Console
- Set up Google Search Console if not already done
- Submit sitemap
- Request indexing for key doc pages
- Monitor indexing progress over 2-4 weeks