- date
- entry
- 016
- topic
- tooling
- rev
- 1
雙語靜態站:canonical 各自指自己,關係用 hreflang 說
這個站中英各一份。設定時最容易犯的錯,是把英文版的 canonical 指向中文版——那句話的意思是「英文版不必收錄」,於是它就真的不會出現在搜尋結果裡。
兩個標籤回答的是不同問題:
canonical——這一頁的正式網址是哪一個?(處理同一份內容有多個網址的情況:帶追蹤參數的、大小寫不同的、有無斜線的)hreflang——這份內容還有哪些語言版本?
把第二件事交給 canonical 做,就是在說「英文版和中文版是同一頁的兩個網址,請只收錄中文版那個」。搜尋引擎會照做。
正確的配置
中文版的 <head>:
<link rel="canonical" href="https://example.com/posts/foo/" />
<link rel="alternate" hreflang="zh-Hant" href="https://example.com/posts/foo/" />
<link rel="alternate" hreflang="en" href="https://example.com/en/posts/foo/" />
<link rel="alternate" hreflang="x-default" href="https://example.com/posts/foo/" />
英文版的 <head>——注意 canonical 換了,hreflang 三行一字不改:
<link rel="canonical" href="https://example.com/en/posts/foo/" />
<link rel="alternate" hreflang="zh-Hant" href="https://example.com/posts/foo/" />
<link rel="alternate" hreflang="en" href="https://example.com/en/posts/foo/" />
<link rel="alternate" hreflang="x-default" href="https://example.com/posts/foo/" />
三個要點:
- canonical 一律自指。每一頁指向自己,兩個語言版本各自被獨立收錄
- hreflang 清單包含自己。中文版也要列出
zh-Hant指向自己,不是只列對方 x-default指向語言未匹配時的落點。我指中文版,因為主要讀者在台灣
為什麼必須雙向
hreflang 是一種宣告,而單向的宣告不被採信。中文版說「我的英文版在那裡」,如果英文版沒有回指中文版,搜尋引擎不會認這組關係——因為任何人都可以宣稱自己是別人的翻譯版。
這對手工維護的靜態站是個實際問題:新增一篇文章要改的地方分散在兩份 HTML 裡,只改一邊不會有任何錯誤訊息,只會安靜地失效。跟 schema 漂移是同一種失效方式——沒有機制會告訴你它壞了。
還有一個容易漏的細節:<html lang> 也要跟著換。
<html lang="zh-Hant"> <!-- 中文版 -->
<html lang="en"> <!-- 英文版 -->
這個屬性不是給搜尋引擎看的,是給螢幕閱讀器和瀏覽器的翻譯功能看的。設錯了,朗讀會用錯的發音規則念整頁。
feed 要分兩份
RSS 的 channel 有一個 <language> 欄位,它是整份 feed 的屬性,不是單篇的。所以把中英文章混在同一份 feed 裡,那個欄位一定是錯的——不管你填哪個值。
兩份各自完整:
<!-- /feed.xml -->
<language>zh-tw</language>
<atom:link href="https://example.com/feed.xml" rel="self" />
<!-- /en/feed.xml -->
<language>en</language>
<atom:link href="https://example.com/en/feed.xml" rel="self" />
各語言的頁面在 <head> 裡也要指向自己語言的那份,不要兩邊都指中文版。訂閱者選了英文版就應該只收到英文。
sitemap 用 xhtml:link 表達同一組
sitemap 裡每個語言版本各自是一筆 <url>,而每一筆都用 xhtml:link
把整組列出來——內容跟 HTML 裡那三行 hreflang 一樣:
<url>
<loc>https://example.com/posts/foo/</loc>
<xhtml:link rel="alternate" hreflang="zh-Hant" href="https://example.com/posts/foo/"/>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/posts/foo/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/posts/foo/"/>
<lastmod>2026-09-06</lastmod>
</url>
記得 <urlset> 要宣告 xmlns:xhtml,漏了整份 sitemap 會解析失敗——而失敗的表現是搜尋引擎默默不處理,你在自己的瀏覽器裡看它是好的。
手工維護就要有檢查
上面這些加起來,一篇新文章要同步的地方有六處:兩份 HTML、兩個列表頁、兩份 feed。六個地方裡漏一個,站就處於不一致狀態,而且不會有人通知你。
值得在 build 之後跑一段檢查,內容大致是:
- 每一頁的 canonical 是否指向自己的網址
- 每一頁是否有三條 hreflang,且中英兩份的那三條完全相同
- 兩份 feed 的項目數與文章目錄數是否一致
- 所有站內連結是否指得到真的存在的檔案
這幾條都可以在幾十行腳本裡跑完。重點跟 /version 那篇一樣:讓「一致」這件事由機器判斷,而不是由人記得。
順帶一提 lastmod。新增一篇文章會同時改動首頁(列表多一項),所以首頁那筆的
lastmod 也要跟著更新。這格最常被漏掉,因為改的人心裡想的是「我只是加了一篇文章」。
不要用機器翻譯充數
最後一件跟技術無關但更重要的事:hreflang 只是宣告兩份內容互為語言版本,它不會讓其中一份變得值得讀。
我的兩個版本是分別寫的,不是翻譯的——因為同一件事講給不同語言的讀者聽,該舉的例子和該省略的背景本來就不一樣。如果沒有打算認真寫第二個語言,只做一種語言是更好的選擇:一份好的內容,勝過一份好的加一份湊數的。
如果只記得一件事
canonical 說「我是誰」,hreflang 說「我還有哪些分身」。
前者一律自指,後者兩邊對稱且包含自己。這兩句話涵蓋了雙語站九成的設定錯誤。
修訂紀錄
- sitemap 改為 build 時產生,同步點由七處減為六處
- 首次發布