Next.js で動的/静的ルーティングのサイトマップを生成する
概要
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}
ディレクトリを設けます。
実装
静的サイトマップページ
-
next-sitemap
のインストールnpm install --save-dev next-sitemap
-
設定ファイル(
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
に追加したいため、使用していません。 -
自動ビルド設定(
package.json
){ "scripts": { "dev": "next", "build": "next build", "start": "next start", + "postbuild": "next-sitemap" },
以上の実装で静的サイトマップページが生成されます。
動的サイトマップページ
※ 前提: TypeScript を使用しています
-
ファイル設置
pages/sitemap/[yyyy]/index.xml.tsx
を設置します。 -
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;
公式の設定方法に則り実装します。
エラーの場合はカスタムエラーページに遷移させたいので、その実装を追加しています。
-
動的 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
で設定しています。
サイトマップインデックスページ
-
ファイル設置
pages/sitemap/index.xml.tsx
を設置します。 -
ファイル設定
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
を使用した実装方法はこちらの記事を参考にしています。 -
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