Skip to content

feat(seo): prerender both languages and fix discoverability - #12

Merged
arnaud-zg merged 1 commit into
feat/i18n-en-frfrom
feat/seo-prerender
Sep 2, 2026
Merged

arnaud-zg merged 1 commit into
feat/i18n-en-frfrom
feat/seo-prerender

Conversation

@arnaud-zg

Copy link
Copy Markdown
Owner

Stacked on #10.

Summary

  • Static HTML for all four pages now (/, /shopping-cart/, /fr/, /fr/shopping-cart/),
    generated at build time instead of shipping an empty #app div for crawlers
  • scripts/prerender.mjs: SSR build of a new entry-server.tsx, renders each route x
    locale with prerenderToNodeStream (renderToString can't wait for the code-split route
    components to resolve), writes real markup into dist
  • main.tsx hydrates that markup instead of replacing it
  • per page <title>, description, canonical, hreflang alternates, OG and Twitter tags,
    JSON-LD (WebSite for home, ItemList and BreadcrumbList for the shop)
  • sitemap.xml generated with all four URLs and hreflang, robots.txt points at it,
    localized 404 redirect, manifest name cleaned up

Getting a clean hydration took three separate router bugs: the memory history entry needs
the real basepath-prefixed URL for links to render consistently, the canonical path's
trailing slash has to be stripped before it becomes a route path, and the SSR bundle needs
single file output or route-level code splitting gives each chunk its own copy of
LocaleContext, silently wrong locale and all.

flowchart LR
  A[vite build] --> B[dist with empty #app]
  B --> C[prerender.mjs renders each route x locale]
  C --> D[real HTML per URL]
  D --> E[main.tsx hydrates instead of rendering fresh]
Loading

Test plan

  • pnpm typecheck
  • pnpm check
  • pnpm test:coverage, 22 files, 120 tests, thresholds pass
  • pnpm build, diffed all 4 dist/**/index.html for real content and correct locale
  • pnpm test:e2e, new prerender.spec.ts asserts zero hydration console errors and
    real markup on all 4 pages

🤖 Generated with Claude Code

https://claude.ai/code/session_014BcGPRc3sgUJYKihGRXh5g

Static HTML for all four pages now, generated at build time with real
router matches instead of an empty `#app` div. `scripts/prerender.mjs`
does an SSR build of a new `entry-server.tsx`, renders each route x
locale with prerenderToNodeStream (renderToString can't wait for the
code-split route components to resolve), and writes real HTML plus
per-page title, description, canonical, hreflang, OG/Twitter tags and
JSON-LD into dist. main.tsx hydrates that markup instead of replacing it.

Getting there took three separate router-level bugs: history entries
need the real basepath-prefixed URL (not the bare route path) for
links to resolve consistently, canonicalPath's trailing slash has to
be stripped before it's used as a route path, and the SSR bundle needs
single-file output or route-level code splitting gives each chunk its
own copy of LocaleContext.

sitemap.xml lists all four URLs with hreflang, robots.txt points at it,
manifest.json's name drops its em dash, and the 404 redirect page shows
a French message for /fr paths.

```mermaid
flowchart LR
  A[vite build] --> B[dist with empty #app]
  B --> C[prerender.mjs: SSR each route x locale]
  C --> D[dist/index.html, /shopping-cart/, /fr/, /fr/shopping-cart/]
  D --> E[main.tsx hydrates instead of rendering fresh]
```

## Test plan
- pnpm typecheck
- pnpm check
- pnpm test:coverage, 22 files, 120 tests, thresholds pass
- pnpm build, then manually diffed all 4 dist/*/index.html files for
  real content and correct locale
- pnpm test:e2e, including new prerender.spec.ts asserting zero
  hydration console errors and real markup on all 4 pages

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BcGPRc3sgUJYKihGRXh5g
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant