From ca4f8962563ea4f300fa26e52e34144397366949 Mon Sep 17 00:00:00 2001 From: Anthony GAEREMYNCK <1@anthony.sh> Date: Sun, 2 Aug 2026 11:12:21 +0200 Subject: [PATCH] fix(seo): render each page in its actual language during SSR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit i18n.changeLanguage(lang) previously only ran in a client-only useEffect, so every page's SSR pass (Header, Footer, HomePage hero text) always used i18next's shared default language ('fr') regardless of the actual route — meaning /en/ and every English conversion page served French body content to crawlers, undermining the whole per-language SEO effort. Fixed by cloning the i18next instance per page (i18n.cloneInstance({ lng }) via I18nextProvider) instead of mutating the shared singleton. Mutating the shared instance directly during render would have been unsafe anyway: the SSG build renders up to `concurrency` pages in parallel in the same process, so one page's language change would leak into others rendering at the same time. Verified across all 30 pairs x 2 languages that server- rendered body content now matches the page's actual language, with no cross-page leakage. This likely also explains the intermittent React hydration error #418 reported in production: a client that hydrates against SSR content whose language could shift depending on render-order timing is a hydration mismatch waiting to happen. --- frontend/src/components/LangLayout.jsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/LangLayout.jsx b/frontend/src/components/LangLayout.jsx index 071ba67..e3ee3f1 100644 --- a/frontend/src/components/LangLayout.jsx +++ b/frontend/src/components/LangLayout.jsx @@ -1,5 +1,6 @@ -import { useEffect } from 'react'; +import { useMemo } from 'react'; import { Outlet } from 'react-router-dom'; +import { I18nextProvider } from 'react-i18next'; import i18n from '../i18n.js'; import { Header } from './Header.jsx'; import { Footer } from './Footer.jsx'; @@ -8,20 +9,19 @@ import '../styles/layout.css'; export function LangLayout({ lang }) { const { theme, toggleTheme } = useTheme(); - - useEffect(() => { - if (i18n.language !== lang) { - i18n.changeLanguage(lang); - } - }, [lang]); + // A per-page cloned instance, not a mutation of the shared `i18n` singleton: + // the SSG build renders many pages concurrently in the same process + // (ssgOptions.concurrency), so changing the shared instance's language + // during render would leak between pages rendering at the same time. + const scopedI18n = useMemo(() => i18n.cloneInstance({ lng: lang }), [lang]); return ( - <> +