Skip to content

feat(i18n): path-based multilingual top-level pages#42

Merged
miquelmatoses merged 1 commit into
mainfrom
feat/multilingual-top-level-pages
Jun 1, 2026
Merged

feat(i18n): path-based multilingual top-level pages#42
miquelmatoses merged 1 commit into
mainfrom
feat/multilingual-top-level-pages

Conversation

@miquelmatoses
Copy link
Copy Markdown
Collaborator

Summary

Phase 17.11. Every top-level page (home, about, instruments, roles, science, faq, privacy) now exists as a real per-locale path (/es/, /es/about/, ...), prerendered with HTML in that language, so Google can index the national homes as independent pages instead of only ?lang= alternates of EN.

What changed

  • Routing (src/App.jsx): /:lang/<page> routes for ca/es/fr/de/da reusing the same components; EN stays unprefixed. A central useLocaleSync keeps i18n + <html lang> in step with the URL (priority: path prefix > ?lang= > saved/browser).
  • usePageMeta: canonical is locale-prefixed and self-pointing; hreflang is path-based for all 6 languages + x-default (no ?lang=). /about?lang=es canonicalises to /es/about/ — old links keep working, the path is authoritative.
  • HomePage: gains usePageMeta — EN keeps the SEO-rich shell title; localized homes compose a title from existing translated strings (no new copy).
  • LanguageToggle: navigates path-based for every localizable page, not just the blog.
  • Prerender: STATIC_ROUTES × 5 languages.
  • Sitemap: one <loc> per language for each top-level page + blog index (6× each), path-based, 0 ?lang=. 112 → 152 entries.
  • index.html shell: home hreflang switched to path-based; stale ?lang= comment fixed.
  • Shared src/utils/locale.js: single source of truth for path↔locale mapping.

Excluded (with reason)

witness (no public landing — /witness-setup is auth-gated, /witness/:token is a per-token noindex assessment), instrument test pages (interactive, not prerendered), auth/account/admin (private).

Backlog (documented in the sitemap script)

Blog articles still emit a single <loc> with path-based hreflang alternates; promoting them to per-language <loc> like the top-level pages is deferred.

Test plan

  • vite build clean; vitest run — 223 passed (+ locale.test.js)
  • pytest api/ — 324 passed (test_seo sample now 43 routes incl. localized; new test_multilingual_pages.py)
  • Local build:full: dist/es/about/index.html has <html lang="es">, self-canonical /es/about/, full path-based hreflang; /es/ title Cèrcol — Conócete mejor.
  • CI frontend job (build:full + guards)
  • Post-deploy: /es/, /es/about/ return 200 with correct lang/title; sitemap per-lang <loc>; /?lang=es canonical → /es/

Safeguard

No backend/server/DB changes. Shared-server untouched.

🤖 Generated with Claude Code

Phase 17.11. Every top-level page (home, about, instruments, roles,
science, faq, privacy) now exists as a real per-locale path
(/es/, /es/about/, ...), prerendered with HTML in that language, so
Google can index the national homes as independent pages instead of only
as ?lang= alternates of the English version.

Routing (src/App.jsx): adds /:lang/<page> routes for ca/es/fr/de/da
reusing the same page components; EN stays unprefixed. A central
useLocaleSync keeps i18n and <html lang> in step with the URL (path
prefix > ?lang= > saved/browser), so a localized page ships
<html lang="es"> instead of always "en".

usePageMeta (src/hooks/usePageMeta.js): canonical is now locale-prefixed
and points to the page itself; hreflang alternates are path-based for all
six languages plus x-default (no more ?lang=). /about?lang=es
canonicalises to /es/about/, so old external links keep working but the
authoritative URL is the path.

HomePage gains usePageMeta: the English home keeps its SEO-rich shell
title; the localized homes compose a title from existing translated
strings (no new copy). LanguageToggle now navigates path-based for every
localizable page, not only the blog.

Prerender (scripts/prerender.mjs) emits STATIC_ROUTES x 5 languages.
Sitemap (scripts/generate-sitemap.mjs) lists one <loc> per language for
each top-level page and the blog index (6x each), all path-based, with no
?lang= anywhere. Blog articles still emit a single <loc> with path-based
hreflang alternates (promoting them to per-language <loc> is noted as
backlog in the script).

Shared locale helpers in src/utils/locale.js (single source of truth for
the path<->locale mapping), covered by src/utils/__tests__/locale.test.js.

Tests: test_seo.py dynamic sample now includes the localized top-level
pages; new test_multilingual_pages.py asserts <html lang>, self-canonical
and full hreflang coverage on the prerendered localized files.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@miquelmatoses miquelmatoses merged commit 3aa9712 into main Jun 1, 2026
7 checks passed
@miquelmatoses miquelmatoses deleted the feat/multilingual-top-level-pages branch June 1, 2026 13:54
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