TanStack Start v1で多言語サイトマップを作成する方法

スケールに対応した言語別サイトマップの整理

問題

サイトマップは、検索エンジンがサイト上のすべてのページを発見してクロールするのに役立ちます。言語ごとに数百または数千のページを持つ多言語サイトは、すぐに巨大なサイトマップを生成する可能性があります。すべての言語のすべてのURLを記載した単一ファイルは扱いにくくなり、サイトマッププロトコルで定義されている50,000URLまたは50MBのサイズ制限を超える可能性があります。1つの言語の構造を更新する場合、ファイル全体を再生成して再検証する必要があります。大規模なモノリシックサイトマップは保守が困難で、処理が遅く、言語やコンテンツを追加してもスケールしません。

解決策

サイトマップを複数のファイルに分割し、サイトマップインデックスファイルを使用して多数のサイトマップを一度に送信します。/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を生成するヘルパーを作成する

複数の子サイトマップを指す、サイトマップインデックスを生成する2つ目のユーティリティ関数を構築します。

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",
          },
        });
      },
    },
  },
});

このルートは、言語ごとに1つのサイトマップを指すインデックスを生成し、適切なキャッシュヘッダーを持つXMLとして提供します。

4. 言語固有のサイトマップ用のサーバールートを定義する

ロケールパラメータに基づいて各言語のサイトマップを生成する動的サーバールートを作成します。

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という名前のファイルは、動的なロケールパラメータを受け入れるルートを作成します。各言語固有のサイトマップは独立して生成され、他の言語に影響を与えることなく更新できます。

5. データソースからURLを取得する

プレースホルダーのgetUrlsForLocale関数を、データベース、CMS、またはルート定義から実際のURLを取得するロジックに置き換えます。

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,
  }));
}

この例では、指定されたロケールで公開されたページをデータベースからクエリし、メタデータを含むサイトマップエントリにマッピングします。データモデルとURL構造に合わせて、クエリとマッピングロジックを調整してください。