Skip to content

feat: browser-prerender — full per-route SSR output (body + head + JSON-LD), zero refactor#4

Merged
ProfessorManhattan merged 6 commits into
mainfrom
feat/browser-prerender
Jun 25, 2026
Merged

feat: browser-prerender — full per-route SSR output (body + head + JSON-LD), zero refactor#4
ProfessorManhattan merged 6 commits into
mainfrom
feat/browser-prerender

Conversation

@ProfessorManhattan

Copy link
Copy Markdown
Contributor

The complete crawler-invisibility fix

The app is a client SPA → the static index.html shell 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) serves dist/ (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, no window-guarding 60 components.

Verified (clean build)

/about: #root went 0 chars → 138 words of body content + per-route <title> + JSON-LD present. Satisfies the new validate-body-content + validate-ssr-head gates.

Scope

🤖 Generated with Claude Code

Brian Zalewski and others added 2 commits June 21, 2026 01:11
…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>
@ProfessorManhattan

Copy link
Copy Markdown
Contributor Author

Enhanced to prerender all indexable routes from the sitemap, not just the 12 static ones — dynamic /blog/:slug + /case-studies/:slug posts were still client-only. discoverRoutes() now reads dist/sitemap.xml; path extraction handles both {BUSINESS_URL} placeholders (template) and real domains (generated sites). Verified: 12 → 19 routes incl all 6 blog posts (dist/blog/doe-law-wcag/index.html → 150 words + per-post title). No indexable route is left SEO-collapsed — the fix is now complete site-wide.

…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>
@ProfessorManhattan

Copy link
Copy Markdown
Contributor Author

Wired the prerender into demo-deploys.yml between Build and Deploy (npx playwright install --with-deps chromiumnpm run prerenderwrangler pages deploy dist) — otherwise the script existed but never ran on deploy, so live demo sites would still ship the SEO-collapsed SPA. Deploy is GHA wrangler-action (not CF-Pages-git-integration), so Chromium is available and the browser-prerender works in-pipeline. Now the deployed dist/ actually contains the per-route prerendered HTML. Generated-customer-site deploy pipelines should mirror this build→prerender→deploy order.

…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>
@ProfessorManhattan

Copy link
Copy Markdown
Contributor Author

Closed the last deploy-path hole: the prerender now also runs in deploy-template.mjs + deploy-applied.mjs (inserted before each wrangler pages deploy dist), not just the GHA workflow. So every deploy path in the repo — GHA + both scripts — server-renders body+head+JSON-LD per route before shipping. No path can silently deploy the collapsed SPA. (Operator needs Chromium locally: npx playwright install chromium.)

* origin/main:
  fix: generate the favicon set (was 5 dead refs → 404 on every site) (#3)
  feat: build-gate validators (enforce the completeness checklist + caught a favicon bug) (#1)

# Conflicts:
#	package.json
@ProfessorManhattan ProfessorManhattan merged commit 4709c6c into main Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant