如何在 TanStack Start v1 中创建多语言站点地图

按语言组织站点地图以实现可扩展性

问题

站点地图有助于搜索引擎发现并抓取网站上的所有页面。一个拥有数百或数千个页面的多语言网站会迅速生成庞大的站点地图。将每种语言的所有 URL 都列在一个文件中会变得难以管理,并且可能超过站点地图协议规定的 50,000 个 URL 或 50MB 的大小限制。当你更新某种语言的结构时,必须重新生成并重新验证整个文件。大型的单一站点地图难以维护,处理速度慢,且在添加新语言或内容时无法很好地扩展。

解决方案

将站点地图拆分为多个文件,并使用站点地图索引文件一次提交多个站点地图。在 /sitemap.xml 创建顶级站点地图索引,指向各自的语言专用站点地图,例如 /sitemap-en.xml/sitemap-es.xml。站点地图索引文件的 XML 格式与常规站点地图类似,并由站点地图协议定义。这样可以让每个文件易于管理,支持独立更新每种语言,并且在添加新语言或页面时具备良好的可扩展性。

步骤

1. 创建辅助工具生成站点地图 XML

构建一个实用函数,从 URL 条目数组生成有效的站点地图 XML。

export function generateSitemapXML(urls: Array<{ loc: string; lastmod?: string; changefreq?: string; priority?: number }>): string {
const entries = urls.map(url => {
  let entry = `  <url>
    <loc>${url.loc}</loc>`
  if (url.lastmod) entry += `
    <lastmod>${url.lastmod}</lastmod>`
  if (url.changefreq) entry += `
    <changefreq>${url.changefreq}</changefreq>`
  if (url.priority !== undefined) entry += `
    <priority>${url.priority}</priority>`
  entry += `
  </url>`
  return entry
}).join('
')

return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${entries}
</urlset>`
}

该函数接收一个 URL 对象数组,并返回带有所需命名空间和结构的格式化 XML 字符串。

2. 创建辅助工具生成站点地图索引 XML

构建第二个实用函数,用于生成一个指向多个子站点地图的站点地图索引。

export function generateSitemapIndexXML(sitemaps: Array<{ loc: string; lastmod?: string }>): string {
const entries = sitemaps.map(sitemap => {
  let entry = `  <sitemap>
    <loc>${sitemap.loc}</loc>`
  if (sitemap.lastmod) entry += `
    <lastmod>${sitemap.lastmod}</lastmod>`
  entry += `
  </sitemap>`
  return entry
}).join('
')

return `<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${entries}
</sitemapindex>`
}

站点地图索引使用 <sitemapindex> 根标签,并为每个站点地图包含一个 <sitemap> 条目,每个父标签下有一个 <loc> 子条目。

3. 定义主站点地图索引的服务器路由

/sitemap.xml 创建一个服务器路由,返回包含所有语言专用站点地图的站点地图索引。

import { createFileRoute } from "@tanstack/react-router";
import { generateSitemapIndexXML } from "~/utils/sitemap";

const SUPPORTED_LOCALES = ["en", "es", "fr", "de"];
const BASE_URL = "https://example.com";

export const Route = createFileRoute("/sitemap")({
  server: {
    handlers: {
      GET: async () => {
        const sitemaps = SUPPORTED_LOCALES.map((locale) => ({
          loc: `${BASE_URL}/sitemap-${locale}.xml`,
          lastmod: new Date().toISOString().split("T")[0],
        }));

        const xml = generateSitemapIndexXML(sitemaps);

        return new Response(xml, {
          headers: {
            "Content-Type": "application/xml",
            "Cache-Control": "public, max-age=3600",
          },
        });
      },
    },
  },
});

该路由会生成一个索引,每种语言对应一个站点地图,并以 XML 格式返回,同时设置合适的缓存头。

4. 定义语言专用站点地图的服务器路由

创建一个动态服务器路由,根据 locale 参数为每种语言生成对应的站点地图。

import { createFileRoute } from "@tanstack/react-router";
import { generateSitemapXML } from "~/utils/sitemap";

const BASE_URL = "https://example.com";

async function getUrlsForLocale(locale: string) {
  return [
    { loc: `${BASE_URL}/${locale}`, changefreq: "daily", priority: 1.0 },
    {
      loc: `${BASE_URL}/${locale}/about`,
      changefreq: "monthly",
      priority: 0.8,
    },
    {
      loc: `${BASE_URL}/${locale}/contact`,
      changefreq: "monthly",
      priority: 0.8,
    },
  ];
}

export const Route = createFileRoute("/sitemap-$locale")({
  server: {
    handlers: {
      GET: async ({ params }) => {
        const { locale } = params;
        const urls = await getUrlsForLocale(locale);
        const xml = generateSitemapXML(urls);

        return new Response(xml, {
          headers: {
            "Content-Type": "application/xml",
            "Cache-Control": "public, max-age=3600",
          },
        });
      },
    },
  },
});

服务器路由支持与 TanStack Router 相同的动态路径参数,因此以 $locale 命名的文件会创建一个接受动态 locale 参数的路由。每个语言专用的站点地图都是独立生成的,可以单独更新而不会影响其他语言。

5. 从数据源获取 URL

用实际从数据库、CMS 或路由定义中获取 URL 的逻辑替换 getUrlsForLocale 占位函数。

async function getUrlsForLocale(locale: string) {
  const pages = await db.page.findMany({
    where: { locale, published: true },
    select: { slug: true, updatedAt: true },
  });

  return pages.map((page) => ({
    loc: `${BASE_URL}/${locale}/${page.slug}`,
    lastmod: page.updatedAt.toISOString().split("T")[0],
    changefreq: "weekly",
    priority: 0.7,
  }));
}

本示例会查询数据库中指定 locale 的已发布页面,并将其映射为带有元数据的站点地图条目。请根据你的数据模型和 URL 结构调整查询和映射逻辑。