Toolz 在 Next。js 應用程式路由器上以九種語言運行,hreflang 設定在正確之前經歷了三個錯誤版本。第一個放置了一個模組範圍 alternates 佈局中的物件不能因區域設定而異,因此每個翻譯頁面都宣布英文 URL 為其規範。這就是刪除翻譯的錯誤:它告訴谷歌 /es/pricing 是 的複製品 /pricing 並且不應該被索引,這會悄悄地撤銷翻譯工作,而每一頁仍然渲染正確。
第二個版本修復了規範,並將每個配置的區域設定宣傳為備用區域,包括尚未產生翻譯的區域設定。因此,該網站發布了一個 hreflang 簇命名頁面,這些頁面是西班牙語 URL 下的英文文字。第三個版本,即現在運行的版本,從每頁的實際翻譯覆蓋範圍中導出備用集。
本指南介紹了工作設定:區域設定配置 generateMetadata 實作、網站地圖變體以及上面的三個錯誤,以便您可以跳過它們。它位於下面 完整的 hreflang 指南 與 WordPress 版本.
TL;DR: 在應用程式路由器中,hreflang 來自
alternates.languages回來generateMetadata並且規範必須從活動區域設定構建,而不是在模組範圍內聲明一次。從一個區域設定映射建立兩者,在群組中的每個頁面上發出完整的倒數集,並包含x-default。 僅宣傳內容真實存在的區域設置,或發布指向未翻譯頁面的叢集。這 赫雷夫朗產生器 對於根據經過驗證的參考集檢查渲染輸出非常有用。
Next。js 開箱即用地為您提供什麼?
元資料 API 直接支援 hreflang。返回 alternates.languages 從 generateMetadata 產生 <link rel="alternate" hreflang> 頭上的標籤:
export async function generateMetadata({ params }) {
const { locale } = await params
return {
alternates: {
canonical: 'https://example.com/de/preise',
languages: {
'en-US': 'https://example.com/pricing',
'de-DE': 'https://example.com/de/preise',
'x-default': 'https://example.com/pricing',
},
},
}
}
Next。js 將它們渲染到頭部並處理 x-default 鑰匙沒有特殊的外殼,因為它 元資料 API 參考 文件。它不做的是決定哪些區域設定屬於該集合,保持規範同步,或停止將整個物件置於無法變更的模組範圍內。這些是你必須做對的部分,它們是破裂的部分。
還要注意這一點 metadataBase 影響此處的相對 URL。 Hreflang 需要絕對 URL,因此任一組 metadataBase 並使用路徑,或自己建立絕對 URL。我更喜歡從配置的來源明確建立它們,因為發出生產 URL 的預覽部署是其自己的問題類別。
步驟 1:一張區域設定圖,而且只有一張
下游的一切都是從這個讀出來的。一個區域需要三個事實:路線段、hreflang 的 BCP 47 標籤和 <html lang>,以及目前是否正在發貨。
// i18n/locales.ts
export const DEFAULT_LOCALE = 'en'
export const LOCALES = [
{ code: 'en', hreflang: 'en-US', label: 'English' },
{ code: 'de', hreflang: 'de-DE', label: 'Deutsch' },
{ code: 'fr', hreflang: 'fr-FR', label: 'Français' },
{ code: 'ja', hreflang: 'ja-JP', label: '日本語' },
]
/** '/pricing' -> '/de/pricing', and '/pricing' for the default locale. */
export function localize(path: string, locale: string): string {
return locale === DEFAULT_LOCALE ? path : `/${locale}${path}`
}
的 code 和 hreflang 值是故意分開的欄位。路線段是 de 因為沒有人願意 /de-DE/pricing 在他們的 URL 中,hreflang 值是 de-DE 因為這就是你想宣布的。合併它們要么意味著醜陋的 URL,要么意味著裸露的 URL de 在註釋中,以及添加的時刻 pt-BR 並肩而行 pt-PT 合併根本不起作用。
步驟 2:建立集合的助手
每個頁面都使用一個函數,因此形狀不能在路線之間漂移:
export function hreflangAlternates(path: string, locales: string[], base = SITE_URL) {
const eligible = LOCALES.filter((l) => locales.includes(l.code))
// A single-entry cluster is not a cluster: emit nothing rather than
// advertise a one-sided relationship.
if (eligible.length < 2) return []
const alts = eligible.map((l) => ({
hreflang: l.hreflang,
href: base + localize(path, l.code),
}))
alts.push({ hreflang: 'x-default', href: base + localize(path, DEFAULT_LOCALE) })
return alts
}
其中有兩個決定值得竊取。
不到兩個地點的提前回歸。 僅存在於英文中的頁面根本不應該發出 hreflang。單一自引用註釋並不完全錯誤,但它是噪聲,並且發出它的程式碼版本使每個純英文頁面看起來都像爬行報告中的損壞簇。
x-default 由助手而非呼叫者附加。 我看到的每一個將後備留給呼叫者的實作最終都會有一個忘記它的模板。將其折疊到構建集合的東西中,並且不能忘記。
步驟 3:將其連接到生成元資料
// app/[locale]/pricing/page.tsx
import { localize, hreflangAlternates, SHIPPING_LOCALES } from '@/i18n/locales'
import { SITE_URL } from '@/lib/site'
export async function generateMetadata({ params }) {
const { locale } = await params
const path = '/pricing'
const alternates = hreflangAlternates(path, SHIPPING_LOCALES)
return {
alternates: {
// Built from the ACTIVE locale. This is the line that matters.
canonical: `${SITE_URL}${localize(path, locale)}`,
...(alternates.length
? { languages: Object.fromEntries(alternates.map((a) => [a.hreflang, a.href])) }
: {}),
},
}
}
規範是要凝視的線。它必須是 的函數 locale,這意味著它不能存在於模組範圍常數中,不能存在於根佈局中,並且不能跨區域設定段共享。群組中的每個頁面最終都會有一個相同的頁面 languages 地圖和其自身獨有的規範,這正是形狀 hreflang 和規範 要求。
由於整個組的地圖是相同的,因此互惠是在結構上而不是透過紀律來滿足的。德語頁面列出了德語、英語、法語、日語和 x-default;英文頁面也是如此。沒有可以省略條目的每頁邏輯。
第四步:只宣傳現有內容
這是大多數指南跳過的一步,也是產生我們設定中最差版本的步驟。
如果您的翻譯是非同步產生的,或者某個區域設定人口減少,則配置的區域設定清單和翻譯的內容是不同的集合。宣傳配置的清單意味著在本地化 URL 下發布英文文字頁面的 hreflang。 Google 遵循註釋,在承諾德語的地方找到英語,並且您同時在九個區域製造了重複內容。
修復方法是傳遞真實覆蓋而不是配置:
// Coverage is per-page, not global: this page might be translated
// into three languages while the one next to it has none.
const locales = await localesForPage(path)
const alternates = hreflangAlternates(path, locales)
搭配一下 robots: { index: false, follow: true } 在區域設定中存在但尚未翻譯副本的頁面上。對於任何從您自己的導航到達的人來說,它們都可以訪問,並且它們不會出現在索引中,直到副本著陸。一旦覆蓋範圍填滿,兩者都會自動刪除,無需後續部署。
步驟 5:網站地圖變體
如果您想將 hreflang 排除在頁面標題之外,同一位助手會提供網站地圖路線。一 <loc> 每段內容都有兒童時期的替補,而不是一個 <loc> 每個地點:
// app/sitemap-pages.xml/route.ts
const entries = PAGES.map((path) => {
const alts = hreflangAlternates(path, SHIPPING_LOCALES)
const links = alts
.map((a) => `\n <xhtml:link rel="alternate" hreflang="${a.hreflang}" href="${a.href}"/>`)
.join('')
return ` <url>\n <loc>${SITE_URL}${path}</loc>${links}\n </url>`
})
const xml =
`<?xml version="1.0" encoding="UTF-8"?>\n` +
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" ` +
`xmlns:xhtml="http://www.w3.org/1999/xhtml">\n${entries.join('\n')}\n</urlset>`
的 xmlns:xhtml 聲明開啟 <urlset> 是必需的且容易忘記;如果沒有它,備用條目將被忽略。注意結構:列出一個 <loc> 每個區域設定將檔案乘以區域設定計數,並使每個翻譯都與其自己的規範競爭。相同的發現覆蓋範圍,只是條目的一小部分。
選擇一種方法。如果您為相同的 URL 發出頭標籤和網站地圖條目,則有兩個來源會漂移。我們的是頁面的頭標籤和工具目錄的網站地圖替代品,它們是不相交的集合。
預設區域設定是否獲得 URL 前綴?
這是一個路由決策,它會更改 hreflang 集中的每個 URL,因此在編寫幫助程式之前而不是在編寫幫助程式之後進行。
有兩種策略是常見的。 所需前綴 服務於未前綴的預設區域設定 /pricing 還有其他一切都在 /de/pricing. 總是前綴 服務於一個細分市場下的每個區域,包括 /en/pricing,與 /pricing 重定向。 Next。js 中間件同時支援兩者,next-intl 將其公開為 a localePrefix 設定。
如所需要(/pricing, /de/pricing) |
總是(/en/pricing, /de/pricing) |
|
|---|---|---|
| 現有的英文 URL | 保存 | 每個人都成為重定向 |
| 預設區域設定的 HREFLANG | 點位於無前綴 URL | 點數在 /en/... |
| x-預設目標 | 無前綴的根,也是 en 網址 |
必須選擇一個前綴區域設定 |
| 程式碼中的對稱性 | localize() 需要一個預設區域設定分支 |
沒有分支機構;每個地方都是統一的 |
我在現有網站上使用所需的前綴,因為重寫每個索引的英文 URL 以獲得對稱性對於贏得小整潔來說是一筆巨大的成本,並且在流量最高的頁面上重定向不是免費的。在綠地建設中,始終前綴更乾淨: localize helper 失去了它的特殊情況,而且從來沒有一個問題是是否如此 /pricing 和 /en/pricing 是同一頁。
無論哪種方式,hreflang 重要的一點都是一致性。註釋中的 URL、規範中的 URL 以及為 200 提供服務而不重定向的 URL 必須是同一字串。可靠中斷的一種組合始終是前綴,hreflang 仍然指向未前綴的 URL:預設區域設定的每個條目都會命名重定向,並且返回標籤駐留在目的地而不是您命名的 URL 上。
您應該使用哪個庫?
大多數 App Router i18n 設定最終都會開啟 next-intl 或者 next-i18next,兩者都不會為您產生 hreflang。他們解決路由和訊息載入問題;註釋仍然是你的。那很好,因為上面的幫助者是二十行,無論如何你都希望它處於你自己的控制之下。
| 關心 | 由圖書館處理 | 你的 |
|---|---|---|
| 區域設定路由和中間件 | 是的 | |
| 訊息目錄和後備 | 是的 | |
<html lang> |
通常 | 驗證它與 hreflang 值相符 |
alternates.canonical 每個地點 |
不 | 從活動區域設定建置 |
alternates.languages |
不 | 根據區域設定地圖建置 |
x-default |
不 | 附加到幫助程式中 |
需要檢查的圖書館特定事項之一是 <html lang>。 與文件語言相矛盾的結構化資料和 hreflang 比沒有更糟糕,而且佈局是硬編碼的 lang="en" 在服役期間,德語是令人驚訝的常見遺留問題。
你如何驗證它?
建立並讀取實際輸出,因為模板不是證據:
next build && next start
curl -s http://localhost:3000/de/preise | grep -E 'rel="(canonical|alternate)"'
對該輸出進行三次檢查。規範是您取得的 URL。每個區域設定恰好有一個條目加一個 x-default. 每 href 是絕對的,並且與所服務的頁面的 URL 相匹配,包括尾隨斜線。
然後找一個兄弟姊妹,就兩個兄弟姊妹 languages 塊。它們應該是位元組相同的;只是規範不同。如果它們不相同,則頁面中的某些內容是從當前區域設定而不是群組建立集合,這是最常見偽裝的互易失敗。
最後,將集合貼到 hreflang 標籤產生器 驗證程式碼本身。它根據 ISO 形狀檢查每個值,標記無法識別的子標籤,捕獲重複項,並在集合沒有後備時發出警告。它運行客戶端,因此暫存 URL 結構保持私有。這 工具指南 瀏覽工作流程,並且 12 個常見的 hreflang 錯誤 涵蓋標記上線後要尋找的內容。
常見問題
如何在 Next。js 應用程式路由器中新增 hreflang 標籤?
返回一個 alternates.languages 對象來自 generateMetadata,將每個 BCP 47 標籤對應到其絕對 URL,然後設定 alternates.canonical 來自活動區域設定。 Next。js 將兩者渲染到頭部。
為什麼規範必須內建生成元資料?
因為它必須因區域設定而異。模組範圍規範不能,因此每個翻譯頁面都會將預設區域設定 URL 聲明為規範,從而從索引中刪除翻譯。
next-intl 會自動產生 hreflang 標籤嗎?
No。 next-intl 處理路由和訊息載入。 hreflang 註釋和每個區域的規範仍然是您的 generateMetadata.
如何在 Next。js 中新增 x-default?
添加一個 'x-default' 鑰匙 alternates.languages 指向您的後備 URL 的物件。 Next。js 無需特殊處理即可傳遞金鑰。將其附加到建立集合的助手內,以便任何模板都不會忘記它。
hreflang 應該放在腦海中還是 Next。js 中的網站地圖中?
兩者都有效,但不適用於相同的 URL。頭標通過 generateMetadata 更容易調試。網站地圖路線適合大型目錄並保持頁面頭傾斜;記住 xmlns:xhtml 聲明開啟 <urlset>.
如果一個區域設定僅部分翻譯怎麼辦?
僅為該頁面宣傳其內容真實存在的區域,並標記未翻譯的頁面 noindex, follow. 廣告配置的區域設定清單會發布指向未翻譯頁面的叢集。
Next。js 中的 hreflang URL 需要是絕對的嗎?
是的。 Hreflang 需要完全合格的 URL。任一組 metadataBase 或從配置的來源建立 URL,以便預覽部署不會發出生產 URL。
如何檢查渲染的 hreflang 輸出?
運行生產構建,獲取兩個兄弟頁面並進行比較。這 languages 區塊在整個群組中應該是位元組相同的,而每個規範都指向自己的 URL。



