多語 React SEO:讓 canonical、hreflang 與實際路由保持一致

以穩定多語 URL、self-canonical、雙向 hreflang 與可抓取 HTML,修正 React SPA 常見的索引與語言版本混亂。

多語 SEO 的核心不是在頁首加一個語言切換按鈕,而是讓每個語言版本都具備穩定、可抓取的 URL,並用一致訊號說明版本關係。React SPA 最常見的問題是:畫面已切成英文,但 URL、canonical、Open Graph 與 sitemap 仍指向中文;或兩個 URL 的主要內容其實沒有翻譯,卻宣告互為 hreflang。 截至 2026-07-29,Google Search Central 的官方文件仍指出,每個語言版本應列出自己與所有其他版本,hreflang 關係必須能互相返回。Google 也說 hreflang 不是用來偵測頁面語言;主要內容必須真的翻譯。canonical 則用於相似/重複 URL 的代表版本選擇,不能拿它把英文頁 canonical 到中文頁後,又期待英文頁獨立出現在搜尋結果。 實作步驟 第一步選擇 URL 策略。常見且容易驗證的是路徑前綴,例如 /zh-tw/articles/... 與 /en/articles/... ;既有網站也可以保留中文無前綴、英文加 /en/ ,但規則必須全站一致。不要只用 Cookie、localStorage 或 ?lang= 在同一 URL 改主要內容,因為 crawler 與分享連結難以穩定指定版本。 建立中央 route manifest,不要讓每個 React component 自己猜對應頁。manifest 至少包含 route key、各語言 path、標題、描述、是否可索引,以及最後更新時間。語言切換器用 route key 找另一語言 path;找不到真實翻譯時,應回語言首頁或隱藏切換,不要製造不存在的 hreflang。 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() } 第二步讓每個版本 self-canonical。中文頁 canonical 指向中文的唯一正式 URL,英文頁指向英文 URL。移除 tracking parameter、大小寫變體與多餘 trailing slash,redirect 到 canonical 形狀。若頁面尚未翻譯,不要複製原文到英文路徑;可保持 404/410,或提供明確 noindex 的等待頁,但不應宣告為已完成翻譯。 第三步在每個可索引版本輸出完整 hreflang set。中文可使用 zh-Hant 或更精確的 zh-Hant-TW ,英文使用 en ;選擇必須符合內容與目標。每一頁列出自身與對方,也可加 x-default 指向語言選擇或主要入口。若 A 指向 B,B 也必須指回 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" /> 第四步讓 metadata 與內容出現在可抓取的 HTML。純 client-side useEffect 最後雖可能被 renderer 執行,仍會增加延遲與錯誤表面。重要公開 route 應採 SSR、SSG 或 deterministic prerender,讓初始 HTML 已含 title、description、canonical、hreflang、主要標題和內文。每個深層 URL 必須由伺服器回正確狀態:存在回 200,不存在回真正 404,不是所有路徑都回 200 的空 SPA shell。 第五步建立 multilingual sitemap。每個 indexable canonical URL 只出現一次,並可依 Google 文件在 sitemap 內列出 alternate。HTML、sitemap 與 redirect 三者必須使用同一 route manifest,否則很容易一邊更新、另一邊仍指向舊 slug。 第六步處理 runtime 細節。切換語言時更新 <html lang> 、title、description、canonical、hreflang、Open Graph locale 與分享 URL。避免 hydration 後才把中文內容瞬間換成英文;server/prerender 應根據 URL 直接產生正確語言。內部連結用正常 <a href> 或 router 最終輸出的可解析 href,不要只靠 onClick 。 失敗與復原 若搜尋結果選錯語言,先抓取實際 response HTML,不要只看瀏覽器 Elements。確認 URL 回傳 200、HTML 的 lang 、canonical 與 hreflang 是否一致,另一語言是否 reciprocal。若英文 self-canonical 錯指中文,先修 canonical,再送 sitemap;不要用更多 hreflang 疊在矛盾訊號上。 若 deploy 後 metadata 全部變成首頁值,常見原因是 prerender 未等待 route data、head manager 不在正確 provider 內,或 CDN 快取忽略 path/locale。回滾到最後一個有逐 route HTML 的版本,保留錯誤 build 作比對,修復後以 cache-busting request 驗證 origin 與 custom domain。 若某語言大量缺頁,不要 redirect 每個缺頁到首頁並回 200。這會讓使用者與 crawler 都無法區分內容不存在。保留正確 404,從 hreflang 與 sitemap 移除缺少版本;真正翻譯完成後再加入 reciprocal annotations。 驗證指令 以下命令驗證 server 回傳的 HTML,不依賴瀏覽器執行 JavaScript。把 URL 換成 staging 與正式 canonical: curl -sS https://class.lucas-futures.com/articles/example \ | rg &#x27;<html|canonical|hreflang|<title>|<h1&#x27; curl -sS -o /dev/null -w &#x27;%{http_code}\n&#x27; \ https://class.lucas-futures.com/articles/definitely-missing 為每一組翻譯自動檢查:兩頁都回 200;各自 self-canonical;hreflang set 相同且 reciprocal;title/description/H1 使用該語言;不存在頁為 404;sitemap 只列 canonical URL;所有 alternate URL 可解析且沒有 redirect chain。再用 Google Search Console URL Inspection 檢查實際選定 canonical,但把它視為外部驗證,不取代本地 deterministic test。 官方來源 Google:Localized Versions of your Pages Google:Consolidate duplicate URLs 與 canonical Google:JavaScript SEO basics 延伸閱讀 從 技術文章 檢查每個翻譯 slug 與內部連結。 在 課程總覽 學習 React routing、prerender 與自動測試。 若發現索引或 canonical 不一致,可由 聯絡頁 提供可重現 URL。