feat(i18n): path-based multilingual top-level pages#42
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
src/App.jsx):/:lang/<page>routes for ca/es/fr/de/da reusing the same components; EN stays unprefixed. A centraluseLocaleSynckeeps 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=escanonicalises to/es/about/— old links keep working, the path is authoritative.usePageMeta— EN keeps the SEO-rich shell title; localized homes compose a title from existing translated strings (no new copy).STATIC_ROUTES × 5 languages.<loc>per language for each top-level page + blog index (6× each), path-based, 0?lang=. 112 → 152 entries.index.htmlshell: home hreflang switched to path-based; stale?lang=comment fixed.src/utils/locale.js: single source of truth for path↔locale mapping.Excluded (with reason)
witness(no public landing —/witness-setupis auth-gated,/witness/:tokenis 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 buildclean;vitest run— 223 passed (+locale.test.js)pytest api/— 324 passed (test_seo sample now 43 routes incl. localized; newtest_multilingual_pages.py)build:full:dist/es/about/index.htmlhas<html lang="es">, self-canonical/es/about/, full path-based hreflang;/es/titleCèrcol — Conócete mejor./es/,/es/about/return 200 with correct lang/title; sitemap per-lang<loc>;/?lang=escanonical →/es/Safeguard
No backend/server/DB changes. Shared-server untouched.
🤖 Generated with Claude Code