feat: browser-prerender — full per-route SSR output (body + head + JSON-LD), zero refactor#4
Conversation
…put, zero refactor The COMPLETE fix for the crawler-invisibility class. The app is a client SPA: crawlers read the static index.html shell, which has an EMPTY <div id=root> (0 chars) → they see no body content, the homepage head on every route, and no JSON-LD (it's client-rendered). PR #2 fixed the head; this fixes EVERYTHING at once with no component/SSR-compat changes. scripts/prerender-spa.mjs (npm run prerender) serves dist/ via a tiny node http server (SPA fallback) and runs the BUILT app in headless Chromium per route (Playwright — already a dep), capturing the fully-rendered HTML → dist/<route>/index.html. Crawlers (no JS) now get real content; users still hydrate the app. Verified on a clean build: /about went from 0 chars in #root → 138 words of body content + per-route <title> + JSON-LD present. Run in CI/GHA where Chromium is available (the template's e2e CI already has Playwright); supersedes the no-deps head-only prerender (#2, which stays as a Chromium-free fallback). Satisfies the new validate-body-content + validate-ssr-head gates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…case-study posts)
The first cut hardcoded 12 static routes — leaving dynamic /blog/:slug + /case-studies/:slug
posts client-only (still SEO-collapsed). Now discoverRoutes() reads dist/sitemap.xml and
prerenders EVERY indexable URL. Path extraction strips scheme+host OR the {BUSINESS_URL}
placeholder, so it works for both the raw template and generated sites (real domains).
Verified: 12 → 19 routes prerendered, including all 6 blog posts. Sample
dist/blog/doe-law-wcag/index.html → 150 words of body content + per-post <title>. No
indexable route left client-only — the SEO-collapse fix is now COMPLETE across the whole site.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Enhanced to prerender all indexable routes from the sitemap, not just the 12 static ones — dynamic |
…to live sites) PR #4 added scripts/prerender-spa.mjs + 'npm run prerender' but nothing INVOKED it on deploy — the script would have existed but never run, so deployed demo sites would still ship the client-only SPA (SEO-collapsed). Wired it into demo-deploys.yml between Build and Deploy: install Chromium (GHA has it) → npm run prerender → wrangler pages deploy dist. Now the deployed dist contains the per-route prerendered HTML (body + head + JSON-LD). Deploy is GHA wrangler-action (not CF-Pages-git-integration), so Chromium is available — the browser-prerender approach works in this pipeline. Generated-customer-site deploy pipelines should mirror this build→prerender→deploy order. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Wired the prerender into demo-deploys.yml between Build and Deploy ( |
…ed.mjs too Completes the deploy-path coverage. Last commit wired the prerender into the GHA demo-deploys workflow, but the two SCRIPT-based deploy paths (deploy-template.mjs, deploy-applied.mjs) still ran vite build → wrangler pages deploy with NO prerender — so a script-driven deploy would still ship the client-only SPA (SEO-collapsed). Inserted 'node scripts/prerender-spa.mjs' right before each 'wrangler pages deploy dist' (dist/sitemap.xml is present by then, so all routes prerender). Now EVERY deploy path in the repo (GHA workflow + both scripts) server-renders body+head+JSON-LD per route before shipping. Both scripts node --check clean. (Operator needs Chromium locally: npx playwright install chromium — same as the e2e job.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Closed the last deploy-path hole: the prerender now also runs in deploy-template.mjs + deploy-applied.mjs (inserted before each |
The complete crawler-invisibility fix
The app is a client SPA → the static
index.htmlshell has an empty<div id=root>(0 chars). Crawlers + AI-search see: no body content, the homepage<head>on every route, and no JSON-LD (it's client-rendered). PR #2 fixed the head; this fixes all three at once with zero component/SSR-compat changes.How
scripts/prerender-spa.mjs(npm run prerender) servesdist/(tiny node server, SPA fallback) and runs the built app in headless Chromium per route (Playwright — already a dep), saving the fully-rendered HTML →dist/<route>/index.html. The react-snap pattern: no SSR-compat refactor, nowindow-guarding 60 components.Verified (clean build)
/about:#rootwent 0 chars → 138 words of body content + per-route<title>+ JSON-LD present. Satisfies the newvalidate-body-content+validate-ssr-headgates.Scope
dist/.🤖 Generated with Claude Code