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. 언어별 사이트맵을 위한 서버 라우트 정의

로케일 매개변수를 기반으로 각 언어에 대한 사이트맵을 생성하는 동적 서버 라우트를 생성합니다.

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 구조에 맞게 쿼리 및 매핑 로직을 조정하십시오.