Skip to content

Prerender the homepage (/) — implementation plan

Branch: feat/prerender-home. Worktree: /Users/jeffjassky/Projects/scenedetect-prerender-home. Run tests with yarn test from the worktree root (node_modules is symlinked).

Why

/ is served as the bare SPA shell (<div id="app"></div>) plus server-injected JSON-LD. Every non-JS crawler (Bing, PerplexityBot, GPTBot, ClaudeBot) sees no homepage copy at all. The other 14 marketing routes are already snapshotted at Docker-build time by scripts/prerender-marketing.mjs; this plan extends that to / generically — no per-route configuration anywhere.

Current blockers (all verified)

  1. isReady() in scripts/prerender-marketing.mjs requires the captured <title> to differ from the shell's. public/index.html's title is exactly the homepage's title, so / can never pass. It also requires a <script data-managed="pageHead">, which PageHome.vue deliberately does not declare.
  2. snapshotRoute() writes to dist/<route>/index.html; for / that is dist/index.html, i.e. it would overwrite the SPA shell every other route depends on.
  3. server/middlewares/homepage-meta-inject.js answers GET / before the generic prerendered-file handler in server/main.js and always sends the bare shell.
  4. injectIntoHead() in that middleware strips every ld+json script, which would delete any schema a prerendered page carried.
  5. Prerendered pages capture Reveal.vue's sf-reveal--armed (CSS-hidden) state, so most sections ship CSS-hidden to non-JS crawlers. Reveal.vue hides nothing under prefers-reduced-motion.

Work packages

WP-A — prerender script + ready signal + router flag

Files: src/components/mixins/pageHeadMixin.js, scripts/prerender-marketing.mjs, server/router/spa-routes.js, server/router/sitemap-routes.test.js, src/router.js, new scripts/prerender-marketing.test.js.

WP-B — homepage middleware serves the snapshot

Files: server/middlewares/homepage-meta-inject.js, server/middlewares/homepage-meta-inject.test.js.

WP-A and WP-B touch disjoint files and can land in parallel. They share one contract:

Contract: server/router/spa-routes.js exports prerenderOutDir(route) returning the directory name (relative to dist/) that holds index.html for a prerendered route. prerenderOutDir('/') === 'home'; every other route returns route without its leading slash (e.g. '/use-cases/footage-library''use-cases/footage-library'). WP-A implements it. WP-B must consume it (not hardcode 'home'). If WP-B's tests run before WP-A lands, WP-B stubs it in the test only.