🗺️

Next.js で動的/静的ルーティングのサイトマップを生成する

2021/08/24に公開

概要

Next.js のアプリケーションで、CMS などから取得したコンテンツによって動的に生成される URL と、静的な URL の両方がある場合に、サイトマップをどのように自動生成させるかについての記事です。

Zenn に投稿されている記事など、先行事例も見られますが、以下の要件を満たすサイトマップの生成方法については見当たらなかったため、その共有を目的としています。

  • 対象 URL は Next.js の動的/静的ルーティングの両方を含む
    • 動的 URL のページはコンテンツ追加のタイミングでビルドが走らない
  • サイトマップファイルはいくつかに分割し、インデックスファイルも生成する

方針

外製ライブラリの使用

NPM パッケージ next-sitemap を使用します。

静的ページについては Node.js のfsモジュールや、Next.js のpages-manifest.jsonファイルなどを利用した、スクラッチでの実装も考えられますが、Next.js に最適化されたパッケージを使うと便利 & 仕様変更に追従しやすいと考えたため、NPM パッケージを使用します。

スター数の多いものだと next-sitemap-generator などもありますが、以下の観点からnext-sitemapに決定しました。

  • Next.js の Custom Server など、利用したくない機能が使われていない
  • 静的/動的ルーティングの両方に対応している

サイトマップのインデックスページも自動生成する機能を含むものがあればより良いのですが、現行の NPM パッケージでは見つかりませんでした。

next-sitemap の GitHub Issue で対応予定とされているため、アップデートされたら利用したいです。

ファイル設置箇所

pages/配下にインデックスページと動的サイトマップページのファイルを設置します。

Next.js の API Routes 機能を使用してpages/api配下にまとめることもできますが、インデックスファイルを含むディレクトリ構成を直感的に把握できるよう、今回は上記の方針を採用しました。

ページ構成

sitemap.xml
└ sitemap/
  ├ static.xml
    └ ${YYYY}/
         └ index.xml

sitemap.xmlはインデックスページです。

sitemap/static.xmlは静的ルーティングのサイトマップページです。

CMS で投稿されたコンテンツと連動する動的ルーティングのページは、その公開年度に応じてファイルを分割するため、${YYYY}ディレクトリを設けます。

実装

静的サイトマップページ

  1. next-sitemapのインストール

    npm install --save-dev next-sitemap
    
  2. 設定ファイル(next-sitemap.js) の実装

    module.exports = {
      siteUrl: // Yout site URL,
      outDir: './public/sitemap',
      sitemapBaseFileName: 'static',
      exclude: // Your site page paths array,
    };
    

    サイトマップファイルをsitemap/配下に設置し、ファイル名をstaticとするため、上記の設定にします。

    robots.txtを自動生成してくれるオプションもありますが、今回の仕様ではサイトマップインデックスのファイルのみをrobots.txtに追加したいため、使用していません。

  3. 自動ビルド設定(package.json

    {
      "scripts": {
        "dev": "next",
        "build": "next build",
        "start": "next start",
    +   "postbuild": "next-sitemap"
      },
    

以上の実装で静的サイトマップページが生成されます。

動的サイトマップページ

※ 前提: TypeScript を使用しています

  1. ファイル設置

    pages/sitemap/[yyyy]/index.xml.tsx を設置します。

  2. next-sitemapの機能を利用した設定

    import React from 'react';
    import { getServerSideSitemap, ISitemapField } from 'next-sitemap';
    import { GetServerSideProps, NextPage } from 'next';
    import Error from '../../_error';
    
    type ErrorResponse = {
      statusCode: number;
    };
    
    type Props = {
      error?: ErrorResponse;
    };
    
    export const getServerSideProps: GetServerSideProps = async (ctx) => {
      
      const fields: ISitemapField[] = [];
      // TODO: 動的 URL の設定
      return getServerSideSitemap(ctx, fields);
    };
    
    // 純粋な XML を返却するために Page コンポーネントでは null を返す
    // エラーの場合はカスタムエラーページを返す
    const SitemapPage: NextPage<Props> = ({ error }) => {
      if (error?.statusCode) {
        return <Error statusCode={error?.statusCode} />;
      } else {
        return null;
      }
    };
    export default SitemapPage;
    

    公式の設定方法に則り実装します。

    エラーの場合はカスタムエラーページに遷移させたいので、その実装を追加しています。

  3. 動的 URL の設定

    export const getServerSideProps: GetServerSideProps = async (ctx) => {
    
      // 動的 URL データの取得
      const res = await fetch('Your API URL');
      if (!res.ok) {
        ctx.res.statusCode = 500;
        const error = { statusCode: 500, message: '' };
        return { props: { error } };
      }
      const pageData = await res.json();
    
      // 各 URL の XML タグ設定
      const fields: ISitemapField[] = [];
      pageData.forEach((page) => {
        fields.push({
          loc: // Your page URL,
          lastmod: // Your page modified date (W3C datetime format),
          priority: '1.0',
        });
      });
    
      ctx.res.setHeader('Cache-Control', 'max-age=86400'); // 24時間キャッシュ
      return getServerSideSitemap(ctx, fields);
    };
    

    CMS からコンテンツのデータを取得し、getServerSideSitemapの第2引数に格納するデータ形式に加工します。

    ここでは省略していますが、リクエスト URL の年度yyyyが存在しないものであった場合は404ステータスコードのエラーページを返却するよう実装しています。

    priority属性は Google が利用していないため、一律1.0で設定しています。

サイトマップインデックスページ

  1. ファイル設置

    pages/sitemap/index.xml.tsx を設置します。

  2. ファイル設定

    import { GetServerSideProps } from 'next';
    
    // 各 URL の XML タグ生成処理
    const generateSitemapTag = (path: string): string => {
      return `
        <sitemap>
          <loc>${SITE_URL}/sitemap/${path}.xml</loc>
        </sitemap>
      `;
    };
    
    //  XML ファイル生成処理
    const generateSitemapXml = (): string => {
      let xml = `<?xml version="1.0" encoding="UTF-8"?>`;
      xml += `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`;
      xml += generateSitemapTag('static');
    
      // 年度別サイトマップページの追加処理
    
      xml += `</sitemapindex>`;
      return xml;
    };
    
    export const getServerSideProps: GetServerSideProps = async ({ res }) => {
      const xml = generateSitemapXml();
    
      res.statusCode = 200;
      res.setHeader('Content-Type', 'application/xml');
      res.end(xml);
    
      return {
        props: {},
      };
    };
    
    const Page = (): null => null;
    export default Page;
    

    xml の生成に NPM パッケージを使用する方法もありますが、今回はスクラッチで実装しています。

    getServerSidePropsを使用した実装方法はこちらの記事を参考にしています。

  3. rewrites 設定

    ページ URL をサイト直下にしたい場合、以下の設定をnext.config.jsに追加します。

    module.exports = {
      async rewrites() {
        return [
          {
            source: '/sitemap.xml',
            destination: '/sitemap/index.xml',
          },
        ];
      },
    });
    

考察

良かったこと

静的ルーティングのサイトマップを、自動でpages/配下ファイルを走査させる実装無しに、NPM の build コマンドに引っ掛けるだけで自動生成できるのはnext-sitemapの大きな利点でした。
今後、サイトにページを追加していく際にも特に追加設定は必要なく、除外対象ページの設定だけ行えばよいため、運用コストがかからなくて楽だと思います。
今回は使用していませんがrobots.txtを同時生成できるオプションもあるため、SEO 目的のファイルが一元管理できるのも魅力です。

静的サイトであれば、ファイル自動分割機能なども含めてフル活用できますが、今回のような動的ページを含むサイトであっても、上記の恩恵は受けられますし、動的サイトマップ生成の実装では型定義の import ができるので TypeScript との相性も良かったです。

課題

動的サイトマップページも、インデックスページ同様に URL をカスタマイズしたい(rewrite / redirect させたい)ケースが考えられます。

しかし、rewrites 設定を用いた場合に Google Search Console 上でインデックスされない(取得エラーになる)ことが判明しています。

どのような方法で URL のカスタマイズが可能かは調査中です。

Discussion