😀

Next.js Pages Routerのプロジェクトでsitemapを生成する。

に公開

Next.jsのPages Routerのプロジェクトで、 next-sitemap というライブラリを用いてサイトマップを生成していました。しかし、Next.js v15へのアップグレードに伴い、リクエストAPIが非同期になったことなどの影響で動作しなくなったため、今回は自作によるサイトマップ生成へ切り替えました。

1. プロジェクト全体のアーキテクチャ

以下のような構成を前提としています。

  • CMSでデータ管理:
    コンテンツはCMS管理しており、投稿記事などの情報を取得。

  • SSG(静的サイト生成):
    Next.jsのSSGを前提としており、ビルド時に全コンテンツを静的ファイルとして出力。

  • サイトマップも静的ファイルとして出力:
    サイトマップはビルド時に生成し、publicフォルダ内に sitemap.xmlとして保存されます。

  • データフェッチ方法:
    getStaticPropsを利用してビルド時に一度だけデータを取得します。
    ※記事更新時は再ビルドが必要となるため、CI/CDによる自動化

2. sitemap.xml.tsxの作成

まずは、pagesディレクトリ配下にsitemap.xml.tsxというコンポーネントを作成します。
今回は、以下の理由から通常のページコンポーネントとして処理せずに、直接レスポンスとしてXMLを出力する構成としています。

  • notFound の活用:
    getStaticProps の戻り値に notFound: true を設定することで、Next.js による自動ページ生成を抑制します。

  • デフォルトエクスポートとして null を返却:
    ブラウザ側で何もレンダリングされないようにしています。

export const getStaticProps: GetStaticProps = async () => {
  // ここでサイトマップのXMLコードを生成する
  return {
    notFound: true,
  }
}

export default () => null;

3. XML形式のサイトマップ生成とファイル出力

CMSから取得した投稿記事データを利用し、XML形式のサイトマップ文字列を生成します。その後、Node.js の標準モジュールであるfsを使用してファイル出力します。
※generateSitemap関数に関しては後述します。

export const getStaticProps: GetStaticProps = async () => {
  // generateSitemapはデータを渡してXMLを生成する関数、CMSのデータを渡してます
  const sitemap = await generateSitemap(Posts);

  try {
    // XML文字列をpublicフォルダ内にsitemap.xmlとして書き出し
    await fs.promises.writeFile("./public/sitemap.xml", sitemap);
  } catch (err) {
    console.error("サイトマップ生成に失敗しました。", err);
  }

  return {
    notFound: true,
  }
};

export default () => null;

fsモジュールを使ってファイル出力する

generateSitemapにより、XML文字列に組み立てられるので、fsモジュールでファイル出力します。下記の処理で、XML 文字列の内容が、指定されたパスに非同期で書き込まれます。

await fs.promises.writeFile(`書き出し先`, XML文字列の内容)

4. generateSitemap 関数の実装

generateSitemap 関数は、投稿記事情報を基にURLリストを生成し、最終的にXML形式のサイトマップ文字列を返す関数です。

4.1 URL の生成

まず、コンテンツのURLを生成します。

const siteURL = "https://example.com";
const totalPages = 150;

// CMSから取得した投稿データでそれぞれのURLを生成
const postUrls = posts.map(
  (post) => `${siteURL}/${post.slug}`
);
const range = (start: number, end: number): number[] =>
  [...Array(end - start + 1)].map((_, i) => start + i);

const paginationUrls = range(1, totalPages).map(
  (pageNumber) => `${siteURL}/page/${pageNumber}`
);

// 全URLを統合
const allUrls = [siteURL, ...paginationUrls, ...postUrls];

4.2 特殊文字のエスケープして<url>形式に変換

XMLでは、&, <, >, " などの特殊文字がそのまま含まれると構文エラーの原因となるため、これらを適切にエスケープした上で<url>形式に変換します

const escapeHTML = (str: string) =>
  str.replace(/&/g, "&amp;")
     .replace(/"/g, "&quot;")
     .replace(/</g, "&lt;")
     .replace(/>/g, "&gt;");

const urlList = allUrls.reduce(
  (acc, url) => acc + `<url><loc>${escapeHTML(url)}</loc></url>`,
  ""
);

4.3 全体を1つのXML文字列として返します。

return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"
        xmlns:xhtml="http://www.w3.org/1999/xhtml"
        xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
        xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
${urlList}
</urlset>`;

5.sitemap.xmlが生成できたらSeach Consoleで送信

サイトマップファイルが正しく生成されpublic/sitemap.xmlに置かれたら、Google Search Consoleを利用してサイトマップを送信して完了です。
https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap?hl=ja#addsitemap

6.最後に

Next.jsのapp routerでは、v13.3.0から公式にsitemapに関する機能が追加され、サイトマップ生成が簡単にできるようになりました。しかし、Pages router環境ではこの機能が利用できないため、今回ご紹介した自作によるサイトマップ生成の手法が現状最適なアプローチかなと考えています。
CMSデータを活用し、ビルド時に静的ファイルとしてサイトマップを生成する方法は、安定運用が可能なだけでなく、必要に応じたカスタマイズも柔軟に行えるため、まだまだ改善点はありますが、しばらくはこの方法で対応していきたいと思います。

参考

https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview?hl=ja

https://zenn.dev/catnose99/articles/c441954a987c24
https://qiita.com/kulikala/items/135da20465c367348b52

Discussion