fix(seo): render each page in its actual language during SSR
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.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import i18n from '../i18n.js';
|
import i18n from '../i18n.js';
|
||||||
import { Header } from './Header.jsx';
|
import { Header } from './Header.jsx';
|
||||||
import { Footer } from './Footer.jsx';
|
import { Footer } from './Footer.jsx';
|
||||||
@@ -8,20 +9,19 @@ import '../styles/layout.css';
|
|||||||
|
|
||||||
export function LangLayout({ lang }) {
|
export function LangLayout({ lang }) {
|
||||||
const { theme, toggleTheme } = useTheme();
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
// A per-page cloned instance, not a mutation of the shared `i18n` singleton:
|
||||||
useEffect(() => {
|
// the SSG build renders many pages concurrently in the same process
|
||||||
if (i18n.language !== lang) {
|
// (ssgOptions.concurrency), so changing the shared instance's language
|
||||||
i18n.changeLanguage(lang);
|
// during render would leak between pages rendering at the same time.
|
||||||
}
|
const scopedI18n = useMemo(() => i18n.cloneInstance({ lng: lang }), [lang]);
|
||||||
}, [lang]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<I18nextProvider i18n={scopedI18n}>
|
||||||
<Header lang={lang} theme={theme} onToggleTheme={toggleTheme} />
|
<Header lang={lang} theme={theme} onToggleTheme={toggleTheme} />
|
||||||
<main>
|
<main>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</I18nextProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user