如何在 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" />
页面之间的双向链接可确保搜索引擎理解本地化版本之间的关系,从而为每位用户提供最合适的版本。