如何在 TanStack Start v1 中关联不同语言版本

为搜索引擎关联多语言版本

问题

当一个网站为同一内容提供多种语言版本时,搜索引擎默认会将每种语言视为独立页面。如果没有明确的信号将这些版本关联起来,搜索引擎无法理解 /en/about/fr/about 是同一内容的不同译本,而不是相互竞争的重复内容。这种分散会导致各语言版本的排名权重被拆分,并带来展示问题:例如,法语用户可能在搜索结果中看到英文页面排名更高,即使已经有法语翻译。搜索引擎需要明确的元数据来理解各语言版本之间的关系,从而根据用户的语言偏好和地理位置,提供合适的页面版本。

解决方案

在文档 head 中添加 hreflang 链接元素,声明每个页面所有可用的语言版本。这些链接会告知搜索引擎哪些 URL 包含相同内容的不同语言版本,从而合并排名信号,并为用户提供正确的版本。每个页面都要列出所有语言的替代版本,包括自身的引用,形成双向关系,帮助搜索引擎理解翻译结构。此元数据通过框架的 head 管理系统按路由添加,该系统可访问当前路由参数,用于构建所有语言版本的 URL。

步骤

1. 创建辅助函数生成多语言版本的 URL

路由器的 buildLocation 方法会根据路由参数构建完整 URL,你可以用它来生成每种语言版本的 URL。

import { AnyRouter } from "@tanstack/react-router";

export function buildLanguageAlternates(
  router: AnyRouter,
  currentPath: string,
  currentLang: string,
  availableLanguages: string[],
) {
  return availableLanguages.map((lang) => {
    const location = router.buildLocation({
      to: currentPath,
      params: { lang },
    });
    return {
      lang,
      href: `${location.pathname}${location.search}${location.hash}`,
    };
  });
}

此函数接收当前路由路径,并通过将每个语言代码替换到路由参数中,生成对应的备用 URL。

2. 定义可用语言

创建一个配置文件,列出应用支持的所有语言。

export const AVAILABLE_LANGUAGES = ["en", "fr", "de", "es"];

export const DEFAULT_LANGUAGE = "en";

该集中式列表将用于为每个页面生成 hreflang 链接。

3. 在路由的 head 函数中添加 hreflang 链接

head 函数会接收包含 matches、params 和 loaderData 的上下文,可用于访问当前语言参数和路由实例。

import { createFileRoute } from "@tanstack/react-router";
import { buildLanguageAlternates, AVAILABLE_LANGUAGES } from "../i18n-config";

export const Route = createFileRoute("/$lang/about")({
  head: ({ params }) => {
    const alternates = buildLanguageAlternates(
      Route.router,
      "/$lang/about",
      params.lang,
      AVAILABLE_LANGUAGES,
    );

    return {
      links: alternates.map((alt) => ({
        rel: "alternate",
        hreflang: alt.lang,
        href: alt.href,
      })),
    };
  },
  component: AboutPage,
});

function AboutPage() {
  return <div>About page content</div>;
}

hreflang 属性使用 ISO 639-1 语言代码,每个链接都指向同一页面的不同语言版本。

4. 添加 x-default hreflang 以实现回退行为

x-default hreflang 属性用于指示当没有语言匹配时的默认页面。

export const Route = createFileRoute("/$lang/about")({
  head: ({ params }) => {
    const alternates = buildLanguageAlternates(
      Route.router,
      "/$lang/about",
      params.lang,
      AVAILABLE_LANGUAGES,
    );

    const defaultUrl = alternates.find((alt) => alt.lang === "en");

    return {
      links: [
        ...alternates.map((alt) => ({
          rel: "alternate",
          hreflang: alt.lang,
          href: alt.href,
        })),
        {
          rel: "alternate",
          hreflang: "x-default",
          href: defaultUrl?.href || alternates[0].href,
        },
      ],
    };
  },
  component: AboutPage,
});

x-default 链接为语言偏好未匹配任何已声明备用语言的用户提供回退 URL。

5. 应用于带参数的动态路由

对于包含除语言外其他动态片段的路由,生成备用链接时需包含这些参数。

export const Route = createFileRoute("/$lang/posts/$postId")({
  head: ({ params }) => {
    const alternates = AVAILABLE_LANGUAGES.map((lang) => {
      const location = Route.router.buildLocation({
        to: "/$lang/posts/$postId",
        params: { lang, postId: params.postId },
      });
      return {
        lang,
        href: `${location.pathname}${location.search}${location.hash}`,
      };
    });

    return {
      links: [
        ...alternates.map((alt) => ({
          rel: "alternate",
          hreflang: alt.lang,
          href: alt.href,
        })),
        {
          rel: "alternate",
          hreflang: "x-default",
          href:
            alternates.find((a) => a.lang === "en")?.href || alternates[0].href,
        },
      ],
    };
  },
});

每个页面都必须引用其所有语言版本(包括自身),以确保搜索引擎能够识别该内容的完整翻译集合。

6. 验证 hreflang 实现

head 函数返回的 link 元素会由 HeadContent 组件渲染。请检查渲染后的 HTML,确认这些链接已出现在文档 head 区域。

<link rel="alternate" hreflang="en" href="/en/about" />
<link rel="alternate" hreflang="fr" href="/fr/about" />
<link rel="alternate" hreflang="de" href="/de/about" />
<link rel="alternate" hreflang="es" href="/es/about" />
<link rel="alternate" hreflang="x-default" href="/en/about" />

页面之间的双向链接可确保搜索引擎理解本地化版本之间的关系,从而为每位用户提供最合适的版本。