Multilingual React SEO with canonical and hreflang

Keep React routes, self-canonicals, reciprocal hreflang annotations, crawlable HTML, and multilingual sitemaps aligned.

Multilingual SEO is not completed by adding a language switcher to the header. Every language version needs a stable, crawlable URL, and the page must emit consistent signals that describe its relationship to the other versions. A frequent React SPA failure is a screen that visibly changes to English while its URL, canonical, Open Graph data, and sitemap entry still describe the Chinese page. Another is declaring hreflang between two URLs whose primary content was never translated. As of 2026-07-29, Google Search Central&#x27;s official documentation still says that each language version should list itself and every other version, with reciprocal relationships between pages. Google also states that it does not use hreflang or the HTML lang attribute to detect the page language; the primary content must actually be translated. Canonical signals identify a representative among similar or duplicate URLs. Canonicalizing an English page to a Chinese page conflicts with expecting the English URL to be independently selected in search. Implementation steps Choose a URL strategy first. Language path prefixes such as /zh-tw/articles/... and /en/articles/... are explicit and straightforward to test. An existing site may keep its primary Chinese routes unprefixed and add /en/ for English, but that policy must be consistent. Do not rely solely on a cookie, local storage, or ?lang= to replace the primary content at one URL. Crawlers and shared links need a deterministic way to request a version. Build a central route manifest instead of asking each React component to guess its counterpart. At minimum, store a route key, paths for each supported language, title, description, indexability, and update time. The language switcher resolves the other path from the route key. If no real translation exists, return to a language index or hide that choice. Do not generate hreflang for an imaginary page. export const localizedRoutes = { articleIndex: { "zh-TW": "/articles", en: "/en/articles", }, authGuide: { "zh-TW": "/articles/supabase-auth", en: "/en/articles/supabase-auth", }, } as const export function absoluteUrl(pathname: string) { return new URL(pathname, "https://class.lucas-futures.com").toString() } Make each language version self-canonical. The Chinese page points to its one canonical Chinese URL, and the English page points to its English URL. Normalize tracking parameters, case variants, and trailing slashes through redirects to that shape. If a page is not translated, do not copy the original language into the English route. Keep a real 404 or 410, or use an explicit noindex waiting page; do not announce it as a completed translation. Emit a complete hreflang set on every indexable version. Traditional Chinese can use zh-Hant or a more specific target such as zh-Hant-TW , while English can use en ; choose what the content and audience actually represent. Every page lists itself and its counterpart. An optional x-default may point to a language selector or primary entry. If A identifies B, B must identify A. <link rel="canonical" href={absoluteUrl(currentPath)} /> <link rel="alternate" hrefLang="zh-Hant-TW" href={absoluteUrl(routes["zh-TW"])} /> <link rel="alternate" hrefLang="en" href={absoluteUrl(routes.en)} /> <link rel="alternate" hrefLang="x-default" href="https://class.lucas-futures.com/articles" /> Put metadata and substantive content in crawlable HTML. A purely client-side useEffect may eventually run in a renderer, but it increases latency and the failure surface. Important public routes should use SSR, SSG, or deterministic prerendering so the initial response contains the title, description, canonical, hreflang, primary heading, and body. The server must return meaningful status codes for deep URLs: 200 for a real page and a real 404 for a missing one, not a 200 response containing an empty SPA shell for every path. Create a multilingual sitemap from the same route manifest. List each indexable canonical URL exactly once and, if using the sitemap hreflang method described by Google, include its alternates. HTML, redirects, and sitemap generation must share one source of truth; independently maintained copies inevitably leave an old slug in one surface. Finally, update runtime details together. A language transition updates <html lang> , title, description, canonical, hreflang, Open Graph locale, and the share URL. Avoid rendering Chinese on the server and replacing it with English after hydration. The server or prerender should derive language from the URL. Internal navigation should render ordinary resolvable href values rather than click handlers with no crawlable link. Failure and recovery When search selects the wrong language, inspect the response HTML rather than only the browser&#x27;s Elements panel. Confirm the URL returns 200, then compare HTML language, canonical, and hreflang. Visit the counterpart and verify reciprocity. If the English page incorrectly canonicals to Chinese, fix that contradiction before adding more hreflang. When every route suddenly emits home-page metadata after deployment, investigate a prerender that did not await route data, a head manager outside its provider, or a CDN cache key that ignores path or locale. Roll back to the last build with route-specific HTML. Preserve the faulty artifact for comparison, then verify both the origin and a cache-busting custom-domain response after the fix. When one language has many missing pages, do not redirect every missing URL to the home page with status 200. Users and crawlers must be able to distinguish absent content. Keep correct 404 behavior and remove the missing version from hreflang and the sitemap. Add reciprocal annotations only when the real translation exists. Verification commands These commands inspect server-returned HTML without relying on browser JavaScript. Replace the paths with staging and production canonicals: curl -sS https://class.lucas-futures.com/articles/example \ | rg &#x27;<html|canon