Next.jsで5万件を上回る動的生成ページが存在するサイトのサイトマップを生成する

## 前提

[next-sitemap](https://github.com/iamvishnusankar/next-sitemap)@3.1.23を使用。
SSGで生成されるページのサイトマップについては普通にドキュメント通りでできるのでドキュメントを見てほしい。

大量のコンテンツが存在してSSGが非現実的な場合にページをユーザーアクセス時に生成（SSR or ISR）する場合のサイトマップ作成方法について、ドキュメントの記載どおりだとうまく対応できなかったので、以下にメモする。

## 動的生成されるサイトマップに sitemapSize は反映されない

上記のような場合のサイトマップ生成は getServerSideSitemap で動的に行う必要があるが、この関数によって生成されるサイトマップは config の sitemapSize が反映されない。Google は1つのサイトマップXMLごとに最大5万件と規定しているので、ページが5万件を上回るになる場合、これではNG。

5万件を上回らない確証があるならドキュメント通りに対応すれば良いので、以下は読む必要がない。

## 定数

定数はアッパーキャメルケースにしているが、プロジェクトによってアッパーキャメルケースなどに適宜変えてほしい。

```ts
export const SitemapPageSize = 10000;
export const SiteUrl = "https://example.com/";
```


`SitemapPageSize`　は1つのサイトマップXMLに何件のURLを載せるかを定義している。Google は1つのXMLあたり5万件以下と規定しているが、 Vercel で動かす場合、1レスポンス5MBという制限があり、 `50000` だとこれを超えてエラーになることがあった。ホスティングプラットフォームの制約も踏まえつつ、ローカルで生成を試して生成されるサイズを見て余裕持った値にすると良い。

## サイトマップを分割する

```tsx
// pages/server-sitemap/index.tsx
import { getServerSideSitemapIndex } from "next-sitemap";
import { GetServerSideProps } from "next";
import { PrismaClient } from "@prisma/client";
import { SitemapPageSize } from "../../Constants";

export const getServerSideProps: GetServerSideProps = async (ctx) => {
  // 動的生成するURLの件数を取得する（例）
  const prisma = new PrismaClient();
  const count = await prisma.posts.count();
  await prisma.$disconnect();

  const pages = Math.ceil(count / SitemapPageSize);

  return getServerSideSitemapIndex(
    ctx,
    [...new Array(pages)].map(
      (_, i) => `${SiteUrl}server-sitemap/${i}.xml`
    )
  );
};

// Default export to prevent next.js errors
export default function SitemapIndex() {}

```

DBからのデータ取得周りは適当に調整すること。


## 分割されたXMLへのインデックスファイル

```tsx
// pages/server-sitemap/[page].tsx
import { getServerSideSitemap } from "next-sitemap";
import { GetServerSideProps } from "next";
import { PrismaClient } from "@prisma/client";
import { SitemapPageSize } from "../../Constants";

export const getServerSideProps: GetServerSideProps = async (ctx) => {
  // ctx.params?.pageには0.xmlのような値が入っているが、parseIntで数値だけに変換している
  // このpageというのは掲載する各URLではなくサイトマップXMLの単体を指しており、0スタート想定
  // このあたりちょっと雑なので気になるなら適当に直して使ってほしい（編集リクエスト歓迎）
  const page = parseInt(ctx.params?.page as string);
  if (typeof page === "undefined" || isNaN(page)) throw Error();

  // データ取得処理（例）
  const prisma = new PrismaClient();
  const posts = await prisma.posts.findMany({
    select: {
      id: true,
      updatedAt: true,
    },
    orderBy: {
      id: "asc",
    },
    take: SitemapPageSize,
    skip: SitemapPageSize * page,
  });
  await prisma.$disconnect();

  const fields = posts.map((post) => ({
    // 各ページのURLと最終更新日をISOStringで渡す
    loc: `${SiteUrl}${post.id}/`,
    lastmod: post.updatedAt.toISOString(),
  }));

  return getServerSideSitemap(ctx, fields);
};

// Default export to prevent next.js errors
export default function Sitemap() {}

```

DBからのデータ取得周りは適当に調整すること（再）。

## 

```js
/** @type {import('next-sitemap').IConfig} */

module.exports = {
  siteUrl: 'https://example.com/',
  generateRobotsTxt: true,
  // これは上の動的生成時には効かないが、SSGページには効果がある
  sitemapSize: 10000,
  exclude: ['/server-sitemap-index.xml'],
  robotsTxtOptions: {
    additionalSitemaps: [
      `https://example.com/server-sitemap-index.xml`,
    ],
  },
}
```