基礎設施

多語言 SSR SEO:hreflang、canonical、noindex 預算

以多種語言提供同一篇文章會分散您的搜尋排名,除非 `hreflang`、標準網址與 `noindex` 預算能相互配合。本文將說明如何做到。

本文由 AI 模型從英文原文翻譯而來,用字可能與原文有所出入。 閱讀英文原文

將一篇英文文章翻譯成多種語言,並透過伺服器端渲染提供服務,聽起來對搜尋流量來說是個純粹的勝利。這也是一種快速分散您自己排名、用低品質頁面淹沒索引,以及被標記為重複內容的方式。一個依賴搜尋流量的部落格,一切都取決於索引的潔淨度,因此語言層的建構必須以 SEO 為首要考量,而非最後的潤飾。這篇文章將逐步介紹保持索引潔淨的三個控制項:絕對 URL 的生成方式、canonical 和 hreflang 標籤之間的關係,以及 noindex 預算如何決定您實際要公開哪些翻譯版本。

多語系網站中的重複內容陷阱

同一篇文章的五種語言版本,就是五個不同的網址。如果您不小心,搜尋引擎可能會將它們視為相互競爭的內容。這種情況會以兩種截然不同的方式出錯,而且還會相互加劇。

第一種是語言重複。如果韓文和日文版本在結構上幾乎相同,而搜尋引擎無法辨識它們是同一原文的翻譯,它可能會只選擇其中一個、忽略其餘的,或是將您所有內連連結的權重分散到所有版本上。您原本希望每種語言都有一個強勢的頁面,結果卻得到了一個頁面的五個弱勢碎片。

第二種是主機重複,而且比人們預期的更容易觸發。如果您的伺服器根據傳入請求的主機來建立絕對網址,那麼一旦相同的內容可以透過兩個主機名稱存取——例如,一個裸域名和一個 www 子域名,或是一個外洩的測試用別名——搜尋引擎就會看到每個頁面的兩個完整副本。現在,五個語言版本變成了十個或十五個,每一次的分散都會流失排名訊號。這兩個問題的解決方法都始於同一個地方:網站本身對於自己的網址必須有且僅有一種確定的看法。

切勿從請求主機建立絕對 URL

最重要的一條規則很無聊。絕對 URL 是從一個固定的基礎值產生,絕非來自請求。在 Go SSR 伺服器中,這意味著 canonical、hreflang、Open Graph 和 sitemap 的 URL 都來自一個設定好的 SITE_BASE_URL,而 r.Host 絕不會被讀取來建構連結。

// config.SiteBaseURL is the one source of truth, e.g. "https://lucidnote.net".
// Do not read r.Host here. A request arriving on any alias still emits
// the same canonical origin, so the index sees one copy, not one per host.
func absURL(base, lang, slug string) string {
	return base + "/" + lang + "/" + slug
}

如果你從 r.Host 建立連結,每個解析到你伺服器的主機名稱都會在索引中成為你整個網站的平行宇宙。一個固定的基礎值會將所有這些都收攏回一個。這不是一個逐頁的決定或範本的細節。這是整個伺服器的屬性,值得寫一個測試,如果任何處理器為了建立連結而存取請求主機,該測試就會失敗。

一旦每個頁面都同意自己的絕對位址,管理重複內容的兩個標籤——canonical 和 hreflang——就有了堅實的基礎。

canonical 將一個 URL 指定為原始版本

canonical 標籤會告知引擎哪個 URL 是某個內容的權威版本。在一個建構良好的多語言網站上,每個語言頁面都應將 canonical 指向自己。韓文頁面將其 canonical 指向韓文 URL,日文頁面則指向日文 URL。它們彼此之間並非重複內容,而是替代版本,而 hreflang 則另外表達了這種關係。

canonical 發揮作用的地方是在單一語言內部。試想一個可透過追蹤參數存取的頁面,或是一個分類列表,它可能有帶或不帶結尾排序引數的版本。這些變體都應該帶有一個指向乾淨 URL 的 canonical,這樣帶有查詢字串的副本就不會與真實版本競爭。規則很簡單:canonical 解決單一語言內的重複問題,並且基於上述相同的原因,它必須永遠指向一個由固定基礎建構的絕對 URL。

一個常見的錯誤是將每個翻譯版本都設定 canonical 指向英文原文。這會告訴引擎其他語言是英文的重複內容,不應被獨立索引,這會浪費您為了翻譯而期望獲得的流量。每個語言使用自我參照的 canonical 幾乎永遠是您想要的作法。跨語言的關係應交由 hreflang 處理。

hreflang 對稱地對應每種語言版本

hreflang 是一個頁面用來宣告其在其他語言中的兄弟頁面的方式。每個頁面都會為其提供的每種語言(包含自身)列出一個 link rel="alternate",再加上一個 x-default,用來指定當使用者的語言不在您服務範圍內時的備用頁面。搜尋引擎會利用這個資訊,向韓國搜尋者顯示韓文版本,向日本搜尋者顯示日文版本,而不是隨意顯示它先抓取到的那個版本。

以下是一篇文章英文版本的 head 部分,宣告了其四個翻譯後的兄弟頁面和一個預設頁面:

<link rel="canonical" href="https://lucidnote.net/en/multilingual-ssr-seo">
<link rel="alternate" hreflang="en" href="https://lucidnote.net/en/multilingual-ssr-seo">
<link rel="alternate" hreflang="ko" href="https://lucidnote.net/ko/multilingual-ssr-seo">
<link rel="alternate" hreflang="ja" href="https://lucidnote.net/ja/multilingual-ssr-seo">
<link rel="alternate" hreflang="zh-Hans" href="https://lucidnote.net/zh-Hans/multilingual-ssr-seo">
<link rel="alternate" hreflang="zh-Hant" href="https://lucidnote.net/zh-Hant/multilingual-ssr-seo">
<link rel="alternate" hreflang="x-default" href="https://lucidnote.net/en/multilingual-ssr-seo">

有兩個屬性決定了此功能的成敗。第一個是對稱性。如果英文頁面將韓文列為替代版本,那麼韓文頁面也必須將英文列回來。hreflang 是一種相互宣告,搜尋引擎會默默地捨棄單向的設定,因為它無法信任一個沒有指回來源的替代版本。當伺服器渲染這些標籤時,它應該根據該文章可用語言的單一列表,產生完整的替代版本集合,這樣每個版本都會輸出相同、完整且相互對應的區塊。為每個範本手動建立列表,是不對稱性悄悄潛入的原因。

第二個是,這個集合必須反映實際提供的內容。如果某個語言翻譯失敗且頁面不存在,它就不能出現在 hreflang 替代版本中。一個指向遺失或損壞版本的連結,會毒害整個叢集。乾淨的做法是從該 slug 成功發布的語言版本集合中推導出替代版本列表,而不是從網站理論上可以支援的靜態語言列表中推導。

noindex 預算:翻譯所有內容,選擇性地公開

這是大多數指南會跳過的部分。正確設定 hreflang 和 canonical 標籤能讓你安全地索引所有語言版本。但這不代表你應該這麼做。

你邀請索引的每個 URL,都是搜尋引擎會評斷其品質的一個頁面。在一個新網站上,一個總共只有三篇文章的語言版本中,剛翻譯好的頁面就是一個內容貧乏的頁面(thin page):周圍內容很少、沒有同層級的深度、也沒有內部連結指向它。如果在每個語言和分類中索引了數百個這樣的頁面,你被索引的頁面組合的平均品質就會下降。這個平均值很重要。搜尋排名和廣告聯播網的審核都會將網站視為一個整體,而大量內容貧乏的頁面會拖累你真正關心的那些頁面。

因此,我們刻意地運用 noindex 預算。翻譯和儲存的成本低廉,且會對所有語言執行,因為你希望內容在值得公開的那一刻就準備就緒。索引是稀缺資源,它是被授予的,而非自動的。有兩條規則來管理它。

第一條是語言允許清單。可被索引的語言集合,是網站提供服務的語言集合的子集。訪客造訪一個有提供服務但未被索引的語言版本時,仍然會看到一個完整、正確的頁面,只是該頁面帶有 noindex 標籤,並且不會出現在 sitemap 中。假設你提供了五種語言,但只邀請其中兩種內容深度足以獨當一面的語言版本進行索引。

第二條是每個分類的門檻。在特定語言中,一個分類頁面只有在包含至少一定數量的已發布文章(例如三篇)後,才會被索引。低於這個數量,該分類列表根據定義就是內容貧乏的,所以它會被加上 noindex 標籤,並從 sitemap 中移除,直到內容充實為止。翻譯版本依然存在,頁面依然能呈現,URL 對於到達該頁面的人類訪客依然有效。它們只是尚未向爬蟲程式宣告而已。

這樣做的效果是,你翻譯並儲存了所有語言和分類的完整矩陣,但只公開那些內容足夠密集、能帶來幫助而非稀釋品質的單元。

一表看懂政策

每個頁面都屬於幾種狀態之一,而四個 SEO 控制項必須在每種狀態上達成一致。儲存和渲染與索引無關。

頁面狀態 已翻譯並儲存 向訪客渲染 canonical hreflang 備用連結 robots 在 sitemap 中
可索引的語言,密集分類 自身 完整的相互參照集合 index, follow
已提供但不可索引的語言 自身 僅由已索引的同級頁面列出 noindex, follow
文章數低於門檻的分類 自身 其文章的完整集合 noindex, follow
查詢參數變體 不適用 乾淨的 URL 繼承自乾淨的 URL index, follow
翻譯失敗的語言 不適用 在各處皆省略 不適用

最讓人困惑的是第二列。一個不可索引的語言頁面仍然會渲染,並且仍然會透過 hreflang 宣告其同級頁面,但您正在索引的頁面不應將其列為備用連結,因為您不會想將爬蟲指向一個帶有 noindex 標記的目標,並將其視為真正的替代方案。讓已索引的叢集參照已索引的成員,並讓額外的語言版本為直接導覽至該頁面的人類使用者存在。

讓 sitemap、robots、canonical 與 hreflang 保持一致

這四個控制項只有在成套運作時才有效。失敗模式並非單一錯誤的標籤,而是兩個標籤互相矛盾,這對爬蟲程式來說,就像一個混亂的網站,並浪費了您試圖保護的爬取預算。

sitemap 應該只列出您希望被索引的 URL,而不應包含任何帶有 noindex 的內容。一個帶有 noindex 的頁面出現在您的 sitemap 中是個矛盾:您要求爬蟲程式擷取它,然後又告訴它忘記所找到的內容。robots meta 標籤是決定特定頁面是否被索引的權威,而 sitemap 應該要與之保持一致。canonical 應該只指向可被索引的 URL,因此,一個內容貧乏的變體頁面絕不應將自己指定為任何內容的權威版本。而 hreflang,在被索引的叢集中,應該只引用那些本身也被索引的頁面。

讓這些設定保持一致的方法,是在單一地方、計算一次索引決策,並讓所有四個輸出都讀取該決策。當伺服器或擷取步驟決定某個給定的(語言、類別、文章)是可索引的,那個單一的布林值便會驅動 robots 標籤、sitemap 的包含與否、canonical 的目標,以及要發出哪些 hreflang 替代連結。如果每個輸出都自行做出決定,它們就會產生分歧,而這種分歧正是您會因此受到懲罰的原因。

權衡取捨,以及何時該索引所有內容

縮小索引範圍有其真實的成本。較少的索引頁面意味著第一天從搜尋來的進入點較少,因此早期流量會比您公開每個殘缺內容的每種翻譯版本時來得低。您正在用現在的觸及範圍換取長期的品質分數,賭的是一小部分強大的頁面會勝過大量薄弱的頁面,這在一個由搜尋和廣告資助的網站上確實如此。

這個預算並非永久性的。這是一種適合新建立或內容稀疏的網站的狀態,並且應該隨著內容的累積而放寬。某個分類跨越了其文章數量的門檻,並開始自行索引。某個語言在各個分類中累積了足夠的深度,以至於將其加入可索引的允許清單後,不再產生內容貧乏的頁面。控制方式是相同的,只是門檻會變動。

當內容貧乏頁面的風險消失時,您就可以索引所有內容。這種情況發生在某個語言中幾乎每個分類都已達到門檻、內部連結為每個頁面提供了真實的同層級內容脈絡,以及翻譯品質足夠好,以至於母語搜尋者在到達頁面後不會立即跳出時。到那時,noindex 預算已經完成了它的任務,而更廣泛的索引就成為一項資產,而非負債。在此之前,請翻譯整個矩陣,保持 hreflangcanonical 的嚴格與對稱,並將索引花在那些能夠自負其責的頁面上。