如何在 TanStack Start v1 中翻译页面元数据

为搜索和社交平台翻译元数据

问题

页面元数据(标题和描述)会显示在浏览器标签、书签、搜索结果和社交媒体预览中。如果元数据的语言与页面内容语言不一致,用户在访问页面前就会感受到明显的不协调。例如,西班牙语页面在搜索结果中显示英文标题,会让人觉得本地化质量很差。搜索引擎也可能将这种不匹配视为排名信号,从而降低在特定语言搜索结果中的可见性。

这种不一致会在用户发现阶段就破坏体验。用户用自己偏好的语言搜索时,期望元数据能够匹配。如果不一致,在点击前就会降低信任感。

解决方案

使用与页面内容相同的翻译资源来翻译页面元数据。在路由配置中定义一个 head 函数,访问已翻译的字符串并返回本地化后的标题和描述元数据。这样可以确保浏览器界面、搜索结果和实际页面内容的一致性。

通过在 head 函数中使用 react-intl 的消息格式化,元数据能够与翻译流程保持同步,并在切换语言时自动更新。

步骤

1. 创建在 React 组件外格式化消息的辅助工具

head 函数运行在 React 组件树之外,无法使用 hooks。可以创建一个工具,利用 react-intl 的 createIntl 来格式化消息。

import { createIntl, createIntlCache } from "react-intl";

const cache = createIntlCache();

export function formatMetadataMessage(
  locale: string,
  messages: Record<string, string>,
  id: string,
  values?: Record<string, string | number>,
): string {
  const intl = createIntl({ locale, messages }, cache);
  return intl.formatMessage({ id }, values);
}

该辅助工具会按需创建 intl 实例,便于在 head 函数等非 React 场景下使用。

2. 定义元数据翻译键

为每个需要本地化元数据的页面,在翻译文件中添加 title 和 description 键。

export const enMessages = {
  "page.home.title": "Welcome to Our Site",
  "page.home.description": "Discover amazing content in your language",
  "page.about.title": "About Us",
  "page.about.description": "Learn more about our mission and team",
};

export const esMessages = {
  "page.home.title": "Bienvenido a Nuestro Sitio",
  "page.home.description": "Descubre contenido increíble en tu idioma",
  "page.about.title": "Acerca de Nosotros",
  "page.about.description": "Conoce más sobre nuestra misión y equipo",
};

这些键遵循与组件翻译相同的模式,将所有本地化内容集中管理。

3. 为路由添加 head 函数

head 选项中使用 createFileRoute,以返回已翻译的元数据。可从路由参数或 loader 数据中获取当前 locale,然后使用你的辅助函数格式化消息。

import { createFileRoute } from "@tanstack/react-router";
import { formatMetadataMessage } from "../utils/formatMetadataMessage";
import { enMessages, esMessages } from "../i18n/messages";

const messagesByLocale = {
  en: enMessages,
  es: esMessages,
};

export const Route = createFileRoute("/$locale/about")({
  head: ({ params }) => {
    const locale = params.locale || "en";
    const messages = messagesByLocale[locale] || messagesByLocale.en;

    return {
      meta: [
        {
          title: formatMetadataMessage(locale, messages, "page.about.title"),
        },
        {
          name: "description",
          content: formatMetadataMessage(
            locale,
            messages,
            "page.about.description",
          ),
        },
      ],
    };
  },
  component: AboutPage,
});

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

head 函数会在路由匹配期间运行,并返回 TanStack Start 渲染到文档 head 的元数据对象。

4. 使用 loader 数据生成动态元数据

当元数据依赖于获取的数据时,可在 head 函数中访问 loaderData,将动态内容与翻译模板结合。

import { createFileRoute } from "@tanstack/react-router";
import { formatMetadataMessage } from "../utils/formatMetadataMessage";
import { enMessages, esMessages } from "../i18n/messages";

const messagesByLocale = {
  en: enMessages,
  es: esMessages,
};

export const Route = createFileRoute("/$locale/posts/$postId")({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId);
    return { post };
  },
  head: ({ params, loaderData }) => {
    const locale = params.locale || "en";
    const messages = messagesByLocale[locale] || messagesByLocale.en;
    const { post } = loaderData;

    return {
      meta: [
        {
          title: formatMetadataMessage(locale, messages, "page.post.title", {
            title: post.title,
          }),
        },
        {
          name: "description",
          content: post.excerpt,
        },
      ],
    };
  },
  component: PostPage,
});

function PostPage() {
  const { post } = Route.useLoaderData();
  return <article>{post.content}</article>;
}

loader 会在 head 函数运行前获取数据,使你能够将动态值插入到已翻译的元数据模板中。