使用 Next.js 的 Hreflang 标签:我们如何在 118 个页面上发布 30 种语言

让我分享一个让我夜不能寐的数字:3,540。那是 30 种语言乘以 118 个页面。如果我们在 Deluxe Astrology 上同时推出所有这些,Google 会索引数千个薄弱的、未翻译的或机器垃圾页面。我们的排名会下降。相反,我们构建了一个两层翻译门控系统,逐步推出语言,使用 Claude Haiku 进行批量翻译,每种语言的成本约为 $22,并使用 Winston AI 评分进行质量门控。整个系统运行在 Next.js 上,配备 next-intl 中间件、地区感知规范 URL 和 HTML head 以及 XML 站点地图中的 hreflang 标签。这是我们如何完成它的完整分解 -- 每个中间件配置、每个站点地图条目、每个成本计算。

目录

Hreflang Tags in Next.js: How We Ship 30 Languages Across 118 Pages

为什么 Hreflang 标签在 2025 年仍然重要

Google 的语言检测已经变得更好。我会这样说。但"更好"并不意味着"解决"。如果你正在运行地区变体 -- 考虑 pt-BRpt-PT,或 zh-CNzh-TW -- Google 仍然需要明确的信号。没有 hreflang,你会看到来自巴西的葡萄牙语页面蚕食你的葡萄牙定向内容,反之亦然。

以下是数据告诉我们的:

  • 超过 60% 的多语言网站存在 hreflang 配置错误(来源:Ahrefs 国际 SEO 审计研究)
  • 正确的 hreflang 实现可以在 4-6 周内将目标市场的点击率提升 20-30%
  • 没有 hreflang 的网站在语言版本之间经历不可预测的排名轮换,使性能跟踪几乎不可能

对于 Deluxe Astrology,我们目标是 30 种具有不同内容的语言。不是地区变体 -- 而是实际的不同语言。那是 30 个不同的受众,他们需要在搜索结果中找到正确的版本。Hreflang 在这里不是可选的。它是基础。

大多数指南遗漏的事情:你需要在 both HTML <head> 和你的 XML 站点地图中使用 hreflang。不是一个或另一个。两者都需要。Google 已确认他们处理来自多个来源的 hreflang,这里的冗余不是浪费 -- 这是保险。

3,540 页面问题

让我走过塑造我们整个架构的数学。

Deluxe Astrology 有:

  • 118 个页面(核心内容页面)
  • 41 个翻译命名空间(可翻译字符串的逻辑分组)
  • 39 个地区感知 API 路由
  • 30 种目标语言

30 × 118 = 3,540 个总页面变体。

如果我们在第一天推出所有 3,540 个页面,这里会发生什么:

  1. 大多数页面将包含英文回退文本和非英文 URL 路径。Google 将其视为薄弱/重复内容。
  2. Googlebot 会消耗爬虫预算索引数千个低质量页面。
  3. 网站的整体质量信号会下降,甚至会拖累好的英文页面。
  4. 登陆未翻译页面的用户会立即反弹。

这不是理论。我见过在插入 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 所做的)。

Hreflang Tags in Next.js: How We Ship 30 Languages Across 118 Pages - architecture

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|.*\\..*).*)'] 
};

这里要注意几件事:

  1. localePrefix: 'always' -- 每个 URL 都有一个地区前缀。/en/horoscope/de/horoskop 等。没有歧义。这对 hreflang 至关重要,因为每个备用 URL 必须是不同且可预测的。

  2. 第 2 层 noindex -- 未翻译的地区仍然呈现(这些地区的用户仍然可以浏览),但它们获得 noindex 标头。Google 不会在其上浪费爬虫预算。

  3. 匹配器 -- 我们排除 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" />

两个关键细节:

  1. 规范 URL 是特定于地区的。 德文页面的规范是德文 URL,而不是英文。每个语言版本都是其自己的规范页面。
  2. 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 搜索控制台监控。

翻译管道:Claude Haiku + Winston AI

这是经济数据变得有趣的地方。我们需要将 118 个页面翻译成 41 个命名空间的 30 种语言。专业人工翻译是黄金标准,但预算数学很残酷。

管道

  1. 提取 -- 从 41 个命名空间中提取所有可翻译字符串到结构化 JSON
  2. 翻译 -- 通过 Claude Haiku(Anthropic 的快速、便宜的模型)进行批量处理,包含有关域(占星学)、语气和目标受众的上下文
  3. 质量门 -- 通过 Winston AI 的内容检测和质量评分运行翻译内容。阈值:95%+ 或拒绝。
  4. 人工审查 -- 高价值页面(主页、登陆页面、金钱页面)由母语使用者进行手动审查
  5. 升级 -- 一旦所有命名空间通过质量门,地区将从 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 的每种语言成本大约为所有 118 个页面和 41 个命名空间约 $22。那主要是令牌成本 -- 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 周

让我们诚实地说:Claude Haiku 将 30 种语言翻译 118 个页面的 $660 总费用几乎是令人怀疑的便宜。问题是你需要质量门(Winston AI)和人工审查层来使其生产就绪。即使将这些成本纳入 -- Winston AI API 调用可能是 $50-100,高价值页面的人工审查是 $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 仅在一个位置

仅将 hreflang 放在 <head> 或仅在站点地图中是一个错误。同时使用两者。皮带和吊带。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,我们为 118 个页面翻译 41 个命名空间,每种语言约 $22。对于 30 种语言,总费用约为 $660。添加 Winston AI 质量门大约 $50-100 的 API 调用,以及高价值页面的可选人工审查 $500-1,000,你的全部成本在 $2,000 以下。与 $699/月 的 Weglot 或 $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 自 Astro 4.0+ 以来已经内置了出色的 i18n 支持,其静态生成模型实际上简化了 hreflang 实现,因为所有 URL 在构建时都是已知的。我们已经使用两个框架构建了多语言项目。对于具有大量动态内容和 API 路由的网站(如 Deluxe Astrology 的 39 个地区感知端点),Next.js 是更好的选择。对于内容丰富的网站,交互性较少,Astro 开发 可以更快且更高性能。无论框架如何,hreflang 原则都是相同的。