Next.js(Pages Router) v16에서 다국어 사이트맵을 생성하는 방법

확장성을 위해 언어별로 사이트맵 구성하기

문제

사이트맵은 검색 엔진이 웹사이트의 페이지를 발견하고 색인하는 데 도움을 줍니다. 언어당 수백 또는 수천 개의 페이지가 있는 다국어 사이트의 경우, 모든 로케일의 모든 URL을 나열하는 단일 사이트맵은 빠르게 관리가 어려워집니다. 대규모 모놀리식 사이트맵은 사이트맵 프로토콜에서 정의한 50,000개 URL 또는 50MB 크기 제한을 초과하여 유효하지 않게 될 수 있습니다. 제한 내에 있더라도 한 언어의 콘텐츠가 변경될 때마다 방대한 파일을 재생성하고 검증하는 것은 비효율적입니다. 사이트가 성장하고 더 많은 언어나 페이지를 추가함에 따라 이 접근 방식은 확장되지 않습니다.

해결책

사이트맵 인덱스 파일을 사용하여 사이트맵을 계층 구조로 구성합니다. 인덱스 파일은 각각 단일 로케일의 URL을 포함하는 별도의 언어별 사이트맵을 나열합니다. 이 구조는 개별 사이트맵 파일을 관리 가능하게 유지하고 프로토콜 제한 내에 유지합니다. 한 언어의 콘텐츠가 변경되면 해당 언어의 사이트맵만 재생성하면 됩니다. 이 접근 방식은 새로운 언어가 추가될 때 자연스럽게 확장됩니다. 각 언어는 인덱스에서 참조되는 자체 사이트맵을 갖습니다. 검색 엔진은 먼저 인덱스를 크롤링한 다음 개별 언어 사이트맵에 대한 링크를 따라갑니다.

단계

1. 사이트맵 인덱스 페이지 생성

pages 디렉토리에 페이지를 생성하여 getServerSideProps를 사용해 사이트맵 인덱스를 동적으로 생성합니다.

import { GetServerSideProps } from "next";

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

function generateSitemapIndex(locales: string[]): string {
  const sitemapEntries = locales
    .map((locale) => {
      return `
<sitemap>
  <loc>${SITE_URL}/sitemap-${locale}.xml</loc>
  <lastmod>${new Date().toISOString()}</lastmod>
</sitemap>`;
    })
    .join("");

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

export const getServerSideProps: GetServerSideProps = async ({ res }) => {
  const sitemap = generateSitemapIndex(LOCALES);

  res.setHeader("Content-Type", "text/xml");
  res.write(sitemap);
  res.end();

  return {
    props: {},
  };
};

export default function SitemapIndex() {}

인덱스는 <sitemapindex> 루트 요소를 사용하며, 각각 언어별 사이트맵을 가리키는 <loc> 자식 요소를 포함하는 <sitemap> 항목들로 구성됩니다. getServerSideProps 함수는 Content-Type 헤더를 text/xml로 설정하고 XML 응답을 직접 작성합니다.

2. 언어별 사이트맵 페이지 생성

각 언어별 개별 사이트맵을 생성하기 위한 동적 라우트 페이지를 생성합니다.

import { GetServerSideProps } from "next";

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

interface PageData {
  slug: string;
  lastModified: string;
}

async function getPagesByLocale(locale: string): Promise<PageData[]> {
  return [
    { slug: "about", lastModified: "2024-01-15" },
    { slug: "contact", lastModified: "2024-01-20" },
  ];
}

function generateSitemap(locale: string, pages: PageData[]): string {
  const urlEntries = pages
    .map((page) => {
      return `
<url>
  <loc>${SITE_URL}/${locale}/${page.slug}</loc>
  <lastmod>${page.lastModified}</lastmod>
</url>`;
    })
    .join("");

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

export const getServerSideProps: GetServerSideProps = async ({
  params,
  res,
}) => {
  const locale = params?.locale as string;

  const pages = await getPagesByLocale(locale);
  const sitemap = generateSitemap(locale, pages);

  res.setHeader("Content-Type", "text/xml");
  res.write(sitemap);
  res.end();

  return {
    props: {},
  };
};

export default function LocaleSitemap() {}

동적 라우트는 URL 매개변수에서 로케일을 추출하고 해당 언어의 URL만 포함하는 XML을 생성합니다. 각 사이트맵은 단일 로케일의 콘텐츠에만 집중합니다.

3. 로케일별 콘텐츠 가져오기

플레이스홀더 getPagesByLocale 함수를 실제 데이터 소스로 교체하세요.

async function getPagesByLocale(locale: string): Promise<PageData[]> {
  const response = await fetch(
    `https://api.example.com/pages?locale=${locale}`,
  );
  const data = await response.json();

  return data.pages.map((page: any) => ({
    slug: page.slug,
    lastModified: page.updatedAt,
  }));
}

이 함수는 CMS, 데이터베이스 또는 API를 쿼리하여 지정된 로케일의 페이지를 검색합니다. 사이트맵 생성기가 XML 항목으로 변환하는 구조화된 데이터를 반환합니다.

4. 각 사이트맵에 정적 페이지 추가

동적 콘텐츠와 함께 모든 언어에 존재하는 정적 라우트를 포함합니다.

function generateSitemap(locale: string, pages: PageData[]): string {
  const staticPages = [
    { slug: "", lastModified: new Date().toISOString() },
    { slug: "about", lastModified: new Date().toISOString() },
  ];

  const allPages = [...staticPages, ...pages];

  const urlEntries = allPages
    .map((page) => {
      const path = page.slug ? `/${locale}/${page.slug}` : `/${locale}`;
      return `
<url>
  <loc>${SITE_URL}${path}</loc>
  <lastmod>${page.lastModified}</lastmod>
</url>`;
    })
    .join("");

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

정적 페이지와 동적 페이지를 결합하면 각 언어 사이트맵이 완전해집니다. 정적 페이지는 현재 타임스탬프를 마지막 수정 날짜로 사용합니다.

5. robots.txt에서 인덱스 참조

검색 엔진이 발견할 수 있도록 robots.txt 파일에 사이트맵 인덱스 위치를 추가하세요.

User-agent: *
Allow: /

Sitemap: https://example.com/sitemap.xml

인덱스 파일만 나열하면 됩니다. 검색 엔진이 자동으로 개별 언어 사이트맵 링크를 따라갑니다. 이 파일은 public 디렉토리에 배치하세요.