Next.js 中的 Hreflang 標籤:我們如何在 118 個頁面上支援 30 種語言
分享一個讓我夜不能寐的數字:3,540。那是 30 種語言乘以 118 個頁面。如果我們在 Deluxe Astrology 上同時推出所有這些語言,Google 會索引數千個內容稀薄、未翻譯或機器垃圾頁面。我們的排名會直線下降。相反,我們建置了一個兩層翻譯門控系統,逐步推出語言,使用 Claude Haiku 進行批次翻譯,成本約為每種語言 $22,並透過 Winston AI 評分進行品質控制。整個系統運行在 Next.js 上,使用 next-intl 中間件、區域設定感知的標準網址和 HTML head 與 XML 網站地圖中的 hreflang 標籤。這是完整的分解說明 -- 每個中間件設定、每個網站地圖項目、每個成本計算。
目錄
- 為什麼 Hreflang 標籤在 2025 年仍然重要
- 3,540 頁面問題
- 兩層翻譯門控系統
- Next-intl 中間件配置
- HTML Head 中的 Hreflang 實現
- 使用 Hreflang 項目生成網站地圖
- 翻譯管道:Claude Haiku + Winston AI
- 成本比較:我們的方法與替代方案
- 區域設定感知的架構標記
- 會毀掉排名的常見錯誤
- 常見問題

為什麼 Hreflang 標籤在 2025 年仍然重要
Google 的語言偵測已經進步了。我會承認這一點。但「更進步」並不意味著「已解決」。如果你正在運行區域變體 -- 想想 pt-BR 對 pt-PT,或 zh-CN 對 zh-TW -- Google 仍然需要明確的信號。沒有 hreflang,你會看到來自巴西的葡萄牙語頁面蠶食你的葡萄牙目標內容,反之亦然。
以下是資料告訴我們的內容:
- 超過 60% 的多語言網站存在 hreflang 配置錯誤(來源:Ahrefs 對國際 SEO 審計的研究)
- 正確的 hreflang 實現可以在 4-6 週內將目標市場的點擊率提高 20-30%
- 沒有 hreflang 的網站會經歷語言版本之間不可預測的排名輪換,使效能追蹤幾乎不可能
對於 Deluxe Astrology,我們針對 30 種具有不同內容的語言。不是區域變體 -- 實際的不同語言。這是 30 個不同的受眾,他們需要在搜尋結果中找到正確的版本。Hreflang 在這裡不是可選的。它是基礎。
大多數指南遺漏的事情:你需要在 HTML <head> 和 XML 網站地圖中都有 hreflang。不是一個或另一個。兩者都要有。Google 已確認他們從多個來源處理 hreflang,這裡的冗餘不是浪費 -- 它是保險。
3,540 頁面問題
讓我逐步說明塑造我們整個架構的數學。
Deluxe Astrology 有:
- 118 個頁面(核心內容頁面)
- 41 個翻譯命名空間(可翻譯字串的邏輯分組)
- 39 個區域設定感知的 API 路由
- 30 種目標語言
30 × 118 = 3,540 個總頁面變體。
如果我們在第一天推出所有 3,540 個頁面,這是會發生的情況:
- 大多數頁面將包含英文回退文本,帶有非英文 URL 路徑。Google 將此視為內容稀薄/重複。
- Googlebot 將消耗抓取預算來索引數千個低品質頁面。
- 該網站的整體品質信號會下降,甚至會拖累好的英文頁面。
- 登陸未翻譯頁面的使用者會立即反彈。
這不是理論性的。我在一些連接 Weglot 或類似工具的用戶端網站上看過這種情況發生,他們在一夜間翻轉 20 種語言的開關。流量下降了,沒有上升。
解決方案:不要同時推出所有語言。設置門控。
兩層翻譯門控系統
我們將 30 種語言分為兩層,採用根本不同的推出策略。
層級 1:TRANSLATED_LOCALES
這些是 15 種語言,具有完全翻譯的核心頁面,由母語使用者或經過驗證的雙語人士手動審查。
// config/locales.ts
export const TRANSLATED_LOCALES = [
'en', 'es', 'fr', 'de', 'it', 'pt-BR', 'ja', 'ko',
'zh-CN', 'zh-TW', 'ru', 'ar', 'hi', 'tr', 'nl'
] as const;
這 15 種語言獲得:
- 在所有 118 個頁面上的完整 hreflang 標籤
- 包含在 XML 網站地圖中
- 可索引的、標準化的 URL
- 區域設定感知的架構標記
層級 2:DYNAMIC_TRANSLATED_LOCALES
其餘 15 種語言以英文專用佔位符開始。它們不被索引。它們沒有 hreflang 項目。它們不在網站地圖中。
export const DYNAMIC_TRANSLATED_LOCALES = [
'pl', 'sv', 'da', 'fi', 'no', 'cs', 'ro', 'hu',
'el', 'th', 'vi', 'id', 'ms', 'uk', 'bg'
] as const;
export const ALL_LOCALES = [
...TRANSLATED_LOCALES,
...DYNAMIC_TRANSLATED_LOCALES
] as const;
當層級 2 語言完成翻譯管道時 -- Claude Haiku 批次翻譯、Winston AI 品質門控、可選的人工審查 -- 它會升級到層級 1。Hreflang 項目、網站地圖包含和索引指令會自動更新。
// utils/locale-status.ts
export function isLocaleReady(locale: string): boolean {
// Check if all required namespaces have translations
// with Winston AI scores >= 95%
const status = getTranslationStatus(locale);
return status.completedNamespaces >= REQUIRED_NAMESPACES
&& status.minQualityScore >= 0.95;
}
export function getIndexableLocales(): string[] {
return ALL_LOCALES.filter(isLocaleReady);
}
這是關鍵的洞見:你的 hreflang 實現必須是動態的。它不能是在建置時硬編碼的靜態列表(好吧,如果你在區域設定升級時重新建置可以這樣做,這就是我們在 ISR 所做的)。

Next-intl 中間件配置
中間件是區域設定偵測、路由和門控邏輯全部匯聚的地方。以下是我們實際的 middleware.ts:
// middleware.ts
import createMiddleware from 'next-intl/middleware';
import { NextRequest, NextResponse } from 'next/server';
import { ALL_LOCALES, TRANSLATED_LOCALES } from './config/locales';
const intlMiddleware = createMiddleware({
locales: ALL_LOCALES,
defaultLocale: 'en',
localePrefix: 'always',
localeDetection: true
});
export default function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// Extract locale from path
const pathnameLocale = ALL_LOCALES.find(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
// If locale is in Tier 2 and not yet ready,
// serve content but add noindex header
if (
pathnameLocale &&
!TRANSLATED_LOCALES.includes(pathnameLocale) &&
!isLocaleReady(pathnameLocale)
) {
const response = intlMiddleware(request);
response.headers.set('X-Robots-Tag', 'noindex, nofollow');
return response;
}
return intlMiddleware(request);
}
export const config = {
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
};
這裡需要注意幾點:
localePrefix: 'always'-- 每個 URL 都會獲得區域設定前綴。/en/horoscope、/de/horoskop等。沒有歧義。這對於 hreflang 至關重要,因為每個替代 URL 必須是不同且可預測的。層級 2 noindex -- 未翻譯的區域設定仍會轉譯(來自這些地區的使用者仍然可以瀏覽),但它們會獲得
noindex標頭。Google 不會在這些上浪費抓取預算。匹配器 -- 我們排除 API 路由、Next.js 內部和靜態檔案。39 個區域設定感知的 API 路由有自己的區域設定處理。
如果你正在構建類似的東西,我們已經寫了更多關於我們的 Next.js 開發方法以及中間件如何融入架構的內容。
HTML Head 中的 Hreflang 實現
Next.js 14+ 與 App Router 提供 generateMetadata 函數。這是 hreflang 標籤在 HTML <head> 中所在的位置。
// app/[locale]/[...slug]/page.tsx
import { getIndexableLocales } from '@/utils/locale-status';
import { getLocalizedSlug } from '@/utils/slugs';
type Props = {
params: { locale: string; slug: string[] };
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale, slug } = params;
const baseUrl = 'https://deluxeastrology.com';
const pagePath = slug ? `/${slug.join('/')}` : '';
const indexableLocales = getIndexableLocales();
// Build language alternates -- only for indexable locales
const languages: Record<string, string> = {};
for (const loc of indexableLocales) {
const localizedSlug = await getLocalizedSlug(pagePath, loc);
languages[loc] = `${baseUrl}/${loc}${localizedSlug}`;
}
// x-default points to English
languages['x-default'] = `${baseUrl}/en${pagePath}`;
return {
title: await getLocalizedTitle(pagePath, locale),
alternates: {
canonical: `${baseUrl}/${locale}${pagePath}`,
languages
}
};
}
這會生成如下 HTML:
<link rel="canonical" href="https://deluxeastrology.com/de/horoskop" />
<link rel="alternate" hreflang="en" href="https://deluxeastrology.com/en/horoscope" />
<link rel="alternate" hreflang="de" href="https://deluxeastrology.com/de/horoskop" />
<link rel="alternate" hreflang="fr" href="https://deluxeastrology.com/fr/horoscope" />
<!-- ... 12 more indexable locales ... -->
<link rel="alternate" hreflang="x-default" href="https://deluxeastrology.com/en/horoscope" />
兩個關鍵細節:
- 標準網址是特定於區域設定的。 德文頁面的標準網址是德文 URL,不是英文的。每個語言版本都是其自己的標準頁面。
- x-default 始終存在。 它指向英文。如果 Google 無法將使用者的語言與你的任何 hreflang 項目相匹配,x-default 是後備。
使用 Hreflang 項目生成網站地圖
HTML <head> hreflang 是必要的,但不充分。對於具有 3,540 個潛在頁面變體的網站,你還需要 XML 網站地圖中的 hreflang。原因如下:Google 可以從網站地圖中發現 hreflang 關係,而無需先抓取每個頁面。
// app/sitemap.ts
import { MetadataRoute } from 'next';
import { getIndexableLocales } from '@/utils/locale-status';
import { getAllPages } from '@/utils/pages';
import { getLocalizedSlug } from '@/utils/slugs';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://deluxeastrology.com';
const indexableLocales = getIndexableLocales();
const pages = await getAllPages(); // Returns 118 page definitions
const entries: MetadataRoute.Sitemap = [];
for (const page of pages) {
for (const locale of indexableLocales) {
const localizedSlug = await getLocalizedSlug(page.path, locale);
const url = `${baseUrl}/${locale}${localizedSlug}`;
// Build alternates for this specific page
const alternates: Record<string, string> = {};
for (const altLocale of indexableLocales) {
const altSlug = await getLocalizedSlug(page.path, altLocale);
alternates[altLocale] = `${baseUrl}/${altLocale}${altSlug}`;
}
alternates['x-default'] = `${baseUrl}/en${page.path}`;
entries.push({
url,
lastModified: page.updatedAt,
changeFrequency: page.changeFreq || 'weekly',
priority: locale === 'en' ? 0.9 : 0.8,
alternates: {
languages: alternates
}
});
}
}
return entries;
}
這會生成如下 XML:
<url>
<loc>https://deluxeastrology.com/de/horoskop</loc>
<lastmod>2025-01-15</lastmod>
<xhtml:link rel="alternate" hreflang="en" href="https://deluxeastrology.com/en/horoscope"/>
<xhtml:link rel="alternate" hreflang="de" href="https://deluxeastrology.com/de/horoskop"/>
<xhtml:link rel="alternate" hreflang="fr" href="https://deluxeastrology.com/fr/horoscope"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://deluxeastrology.com/en/horoscope"/>
</url>
具有 15 個可索引區域設定和 118 個頁面,那是 1,770 個網站地圖項目。可管理。當所有 30 種語言準備就緒時,它將是 3,540。仍在 Google 的 50,000 個 URL 網站地圖限制內,但我們無論如何都會分成按區域設定的網站地圖以進行更清晰的 Google Search Console 監控。
翻譯管道:Claude Haiku + Winston AI
這是經濟變得有趣的地方。我們需要將 118 個頁面跨越 41 個命名空間翻譯成 30 種語言。專業人工翻譯將是黃金標準,但預算數學很殘酷。
管道
- 提取 -- 將所有可翻譯字串從 41 個命名空間提取到結構化 JSON 中
- 翻譯 -- 透過 Claude Haiku(Anthropic 的快速、廉價模型)進行批次處理,包含有關領域(占星術)、語調和目標受眾的上下文
- 品質門控 -- 透過 Winston AI 的內容偵測和品質評分執行翻譯內容。閾值:95%+ 或拒絕。
- 人工審查 -- 高價值頁面(首頁、登錄頁面、金錢頁面)由母語使用者進行手動審查
- 升級 -- 一旦所有命名空間通過品質門控,區域設定將從
DYNAMIC_TRANSLATED_LOCALES移至TRANSLATED_LOCALES
// scripts/translate-locale.ts
async function translateLocale(targetLocale: string) {
const namespaces = await getNamespaces(); // 41 namespaces
for (const ns of namespaces) {
const sourceStrings = await loadNamespace('en', ns);
const translated = await claude.messages.create({
model: 'claude-3-haiku-20240307',
max_tokens: 4096,
system: `You are a professional translator specializing in astrology content.
Translate from English to ${getLanguageName(targetLocale)}.
Maintain astrological terminology accuracy.
Preserve all interpolation variables like {name} and {date}.`,
messages: [{
role: 'user',
content: `Translate these JSON key-value pairs. Return valid JSON only:\n${JSON.stringify(sourceStrings, null, 2)}`
}]
});
const qualityScore = await winstonAI.analyze(translated.content);
if (qualityScore >= 0.95) {
await saveNamespace(targetLocale, ns, translated.content);
} else {
await flagForReview(targetLocale, ns, translated.content, qualityScore);
}
}
}
Claude Haiku 的每語言成本大約為**$22**,用於所有 118 個頁面跨越 41 個命名空間。那主要是代幣成本 -- Haiku 的定價非常便宜,輸入代幣 $0.25 / 百萬,輸出代幣 $1.25 / 百萬(2025 年定價)。
成本比較:我們的方法與替代方案
這是說服 Deluxe Astrology 團隊的表格:
| 方法 | 30 種語言的成本 | 持續成本 | 品質 | 推出時間 |
|---|---|---|---|---|
| Claude Haiku + Winston AI | 約 $660 總計(每語言 $22) | $0(一次性) | 95%+ 品質門控、關鍵頁面的人工審查 | 2-3 週滾動 |
| Weglot | $0 設置 | $699/月($8,388/年) | 機器翻譯、可編輯 | 即時但風險高 |
| 專業翻譯者 | $150K-$300K(每語言 $5K-10K) | 每語言 $2K-5K 用於更新 | 最高品質 | 3-6 月 |
| DeepL API | 約 $400 總計 | $0(一次性) | 良好但沒有品質門控 | 1-2 週 |
| Google Translate API | 約 $300 總計 | $0(一次性) | 專利內容品質較低 | 1 週 |
坦白說:30 種語言的 Claude Haiku 翻譯總價 $660 幾乎是可疑地便宜。問題在於你需要品質門控(Winston AI)和人工審查層來使其適合生產。即使考慮這些成本 -- 也許 $50-100 用於 Winston AI API 呼叫,$500-1,000 用於高價值頁面的人工審查 -- 你的總成本仍在 $2,000 以下。將其與 Weglot 的 $699/月進行比較。你會在不到 3 個月內收支平衡。
Weglot 和類似服務的真正殺手:它們一次翻譯所有內容。沒有門控。沒有每頁品質控制。你翻轉一個開關,突然 Google 看到 3,540 個頁面,其中許多是平庸的機器翻譯。我們的方法讓我們對其進行精確控制。
我們在 定價頁面上談論更多關於我們如何處理這樣的項目 -- 翻譯管道是更大型 無頭 CMS 開發 架構的一個組件。
區域設定感知的架構標記
這個幾乎讓每個人感到驚訝。你的結構化資料需要與頁面語言相符。一個帶有英文常見問題架構的德文頁面會混淆 Google 對頁面的理解。
// utils/schema.ts
export function generateFAQSchema(
faqs: Array<{ question: string; answer: string }>,
locale: string
) {
return {
'@context': 'https://schema.org',
'@type': 'FAQPage',
'inLanguage': locale, // Critical: must match page locale
'mainEntity': faqs.map((faq) => ({
'@type': 'Question',
'name': faq.question, // Must be in target language
'acceptedAnswer': {
'@type': 'Answer',
'text': faq.answer // Must be in target language
}
}))
};
}
每個支援 inLanguage 的架構類型都應該使用它。對於 Deluxe Astrology,包括:
- FAQPage -- 目標語言的問題和答案
- Article --
inLanguage與區域設定相符 - WebPage --
inLanguage屬性 - BreadcrumbList -- 目標語言的麵包屑名稱
不要只翻譯可見內容並忘記結構化資料。Google 讀取兩者。
會毀掉排名的常見錯誤
缺少 x-default hreflang
我經常看到這個。網站為所有語言實現 hreflang,但忘記 x-default。沒有它,Google 對於語言與任何版本都不相符的使用者沒有後備。始終包含它。始終將其指向你的主要語言(通常是英文)。
URL 與內容中的不一致區域設定
如果你的 URL 說 /fr/horoscope 但頁面內容是英文,因為翻譯尚未載入或回退,Google 會將此標記為軟 404 或內容稀薄。這正是我們建置兩層門控系統的原因 -- 頁面在擁有法文內容之前不會獲得法文 URL。
同時推出所有語言
我已經敲過這個鼓了,但值得重複。同時推出 30 種語言是國際 SEO 中最常見的單一錯誤。即使你的翻譯是完美的,你也是在要求 Google 在一夜間抓取、索引和評估數千個新頁面。分批推出 3-5 種語言。在 GSC 中監控索引。然後添加更多。
非相互 hreflang 標籤
如果頁面 A(英文)透過 hreflang 指向頁面 B(德文),頁面 B 必須指回頁面 A。如果缺少此相互鏈接,Google 會完全忽略 hreflang。當你動態生成這些時(如我們所做的),互惠是自動的。但如果你手動管理它們,請定期審計。
缺少自引用 hreflang
每個頁面必須在其自己的 hreflang 集合中包含自己。德文頁面必須列出 hreflang="de" 指向自己。這在手動實現中很容易遺漏。
Hreflang 只在一個位置
僅在 <head> 或僅在網站地圖中放置 hreflang 是一個錯誤。使用兩者。完全覆蓋。Google 處理兩個來源,如果其中一個無法被抓取,另一個將作為備份。
對於這種規模的項目,擁有一個經驗豐富的團隊有助於避免這些陷阱。如果你正在規劃多語言建置,我們很樂意 討論方法。
常見問題
如果我只有語言差異(不是區域),我需要 hreflang 標籤嗎? 是的。雖然 Google 的語言偵測在 2025 年有所改進,但 hreflang 仍然是告知搜尋引擎要為哪個語言版本提供服務的決定性信號。沒有它,你會冒著 Google 因為英文版本有更多反向連結而向說法文的使用者顯示你的英文頁面的風險。當你擁有 10+ 種語言時,Hreflang 變得更加關鍵 -- 跨語言蠶食的可能性隨著規模的增加而大幅增加。
單個頁面有多少 hreflang 項目太多了?
Google 還沒有發布官方限制,但實際測試顯示,超過 50 個語言變體每頁,你會開始看到邊際收益遞減和偶爾的解析問題。對於我們的 30 語言設置,每個頁面有 31 個 hreflang 項目(30 種語言 + x-default),遠在安全區域內。如果你處理 50+ 個區域和語言組合,考慮僅使用 XML 網站地圖方法來保持 <head> 大小可管理。
我應該在 HTML head、XML 網站地圖或 HTTP 標頭中使用 hreflang?
對於 Next.js 應用程式,使用 HTML <head> 和 XML 網站地圖。HTTP 標頭主要對非 HTML 資源(如 PDF)有用。HTML <head> 方法在抓取時被處理,提供最快的信號。網站地圖充當備份,幫助 Google 發現尚未抓取的替代頁面。我們不建議僅依賴一種方法。
2025 年用 AI 翻譯完整網站的成本是多少? 使用 Claude Haiku,我們以每語言約 $22 的成本翻譯 118 個頁面跨越 41 個命名空間。對於 30 種語言,總共約 $660。添加 Winston AI 品質門控,成本約為 $50-100 的 API 呼叫,以及可選的高價值頁面人工審查,成本為 $500-1,000,你的全包成本低於 $2,000。將其與 Weglot 的 $699/月或專業翻譯服務的 $5,000-10,000 每語言相比。
為什麼使用兩層翻譯門控系統而不是一次翻譯所有內容? Google 將內容稀薄視為可拖累整個域名的負面品質信號。如果你推出 30 種語言但只有 15 種具有品質翻譯,那 15 種翻譯不良的語言會建立大約 1,770 個低品質頁面。兩層系統確保只有符合 95%+ 品質閾值的頁面被索引。語言在翻譯通過品質門控時從層級 2 升級到層級 1,在推出過程中保護你的域名權威。
對於區域設定部分翻譯的未翻譯頁面,我應該如何處理?
對於某些命名空間已翻譯但其他命名空間未翻譯的區域設定,我們回退到英文內容並透過中間件添加 noindex 元標籤。URL 仍會解析(使用者可以訪問它),但 Google 不會索引混合語言頁面。一旦所有必需的命名空間通過品質門控,noindex 標籤就會被移除,hreflang 項目也會被添加。這可防止部分翻譯污染你的索引。
AI 翻譯應該使用什麼品質評分閾值? 我們使用 Winston AI,閾值為 95%+ 品質評分。任何低於此的項目都會被標記為進行人工審查或使用調整的提示進行重新翻譯。實際上,Claude Haiku 在大約 85% 的命名空間批次上首次通過時達到 95%+。其餘 15% 通常因為特定領域術語(不直接翻譯的占星術術語)或複雜的句子結構而失敗。90% 的閾值會讓某些明顯笨拙的短語通過。
我可以用 Astro 代替 Next.js 用於多語言網站與 hreflang 嗎? 絕對可以。自 Astro 4.0+ 以來,Astro 內置了出色的 i18n 支援,其靜態生成模型實際上簡化了 hreflang 實現,因為所有 URL 在建置時都是已知的。我們已經使用兩個框架建立了多語言項目。對於具有大量動態內容和 API 路由的網站(如 Deluxe Astrology 的 39 個區域設定感知端點),Next.js 是更好的選擇。對於內容豐富但互動性較低的網站,Astro 開發 可能更快且性能更高。無論框架如何,hreflang 原則都是相同的。