Next.jsでsitemap.xmlを自動生成する実装を自作する(PageRouterの場合)

# 背景

- Next.jsでも sitemap.xmlを生成したい(WordPress等ならプラグインでいけそう)
- Next.jsでもプラグインがあるが、こんなことのために依存関係を増やすことはあまりしたくない（個人プロジェクトだと尚更）

# 方法

`src/pages/sitemap.xml`に以下を記載

```typescript
import type { GetServerSideProps } from 'next';
import generateSitemap from '@/libs/helper/generateSitemap';

export const getServerSideProps: GetServerSideProps = async ({ res }) => {
  const xml = await generateSitemap();

  // この辺はAPI Routesの書き方とにてます
  res.statusCode = 200;
  // キャッシュは一旦、1日にしておく
  res.setHeader('Cache-Control', 's-maxage=86400, stale-while-revalidate');
  res.setHeader('Content-Type', 'text/xml');
  
　　　　// xmlとしてレスポンス
  res.end(xml);

  return {
    props: {},
  };
};

const SitemapPage = (): void => {};

export default SitemapPage;
```


URLは以下のように管理されてることとします。

```tsx
const url = {
    API_ROUTES:{
        API1: '/api/api1'
    },
    ROUTER_URL_ITEMS_ID: '/items',
    USERS: {
        INDEX: '/users',
        IMAGES: {
            THUMBNAIL: '/users/images/thumbnail'
        }
    }
    ...
}
```
`src/libs/helper/generateSitemap/index.ts`を作成し以下のように記述


```typescript

type SitemapXmlField = {
  path: string;
  lastmod: string;
};

// urlの構造を
const flattenRouter: (
  url: {
    // 4階層まであると仮定
    [key: string]:
      | string
      | { [key: string]: string }
      | { [key: string]: { [key: string]: string } }
      | { [key: string]: { [key: string]: { [key: string]: string } } } } 
  },
  prefix?: string,
) => { [key: string]: string } = (url, prefix = ''): { [key: string]: string } => {
  // urlがstringの場合は、そのまま返す
  if (typeof url === 'string') {
    return { [prefix]: uri };
  }
  
  // urlがobjectの場合は、再帰的に処理
  return Object.entries(url).reduce((result, [key, value]) => {
    const newPrefix = prefix + key;
    if (typeof value === 'object') {
      const flattened = flattenRouter(value, newPrefix);
      return { ...result, ...flattened };
    }
    return { ...result, [newPrefix]: value };
  }, {});
};

// 動的Routeのものを削除
const flattenedRouterURI = Object.values(flattenRouterURI(routerURI)).filter(
  (value) => !value.startsWith('/items')
);

/**
 * sitemap.xmlのうち固定値の部分
 */
const xmlGeneralFields: Array<SitemapXmlField> = flattenedRouterURI.map((key, index) => ({
  path: flattenedRouterURI[index],
  lastmod: new Date().toISOString(),
}));

/**
 * TopLevelAwaitを防止するためにmain関数を作成
 */
const main = async () => {

  /**
   * 動的ルーティングのページデータを取得
   */
  const resDynamicRoutes = {
    items: // 全アイテムを取得するようなfetch関数をここに入れる ,
    // ...その他に動的ルーティングのものがあれば追加
  };

  // 動的ルーティングのページ
  const xmlDynamicFields: SitemapXmlField[] = resDynamicRoutes.items
    .map((item) => ({
      path: `/[動的ルーティングのベースURL]/${item.id}`,
      lastmod: new Date().toISOString(),
    })）
    .concat(
       ...追加のものについて同様の実装
    )

  const generateSitemapXml = async () => {
    // 取得できない場合undefined
    if (Object.keys(resDynamicRoutes).some((key) => key.length === 0)) {
      return undefined;
    }

    // sitemap.xmlのうち固定値の部分と動的ルーティングのページを結合する
    const fields = xmlGeneralFields.concat(xmlDynamicFields);

    let xml = `<?xml version="1.0" encoding="UTF-8"?>`;
    xml += `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`;

    // この辺で文字列結合する
    fields.forEach((field) => {
      const url = new URL(field.path, `${config.appBaseUrl}`);

      xml += `
        <url>
          <loc>${url.toString().replace(/\/$/, '')}</loc>
          <lastmod>${field.lastmod}</lastmod>
          <changefreq>weekly</changefreq>
          <priority>0.7</priority>
        </url>
      `;
    });

    xml += `</urlset>`;

    return xml;
  };

  return generateSitemapXml;
};

const generateSitemap = async () => {
  const sitemap = await main()
    .then((generateSitemapXml) => generateSitemapXml())
    .then((xml) => xml)
    .catch((err) => {
      console.error(err);
      return undefined;
    });
  return sitemap;
};

export default generateSitemap;


```

# 結果

無駄な依存関係を作らないので嬉しい