gladevise Logo
HomeAboutContact

Next.jsでsitemap.xml生成する


Next.jsでsitemap.xmlを生成する方法は大きく分けて2つあります。

1つは、ビルド時にpostbuildでsitemap.xmlを/public/sitemap.xmlに出力する静的な方法、もう1つはServerSideRendering(SSR)を使ってアクセス時にsitemap.xmlを生成する動的な方法です。

この記事ではiamvishnusankar/next-sitemapを使って、2つの方法でsitemap.xmlを生成する方法を紹介した後、SSRを使ってsitemap.xmlを生成するコードを自作する例を紹介します。

サイトマップを生成するブログのテンプレートとして、Next.jsのblog-starter-typescriptを使用します。

事前に以下のコマンドを実行してプロジェクトを作成してください。

npx create-next-app --example blog-starter-typescript blog-starter-typescript-app

また今回、作成するプロジェクトは以下のリポジトリに公開してあります。

https://github.com/gladevise/next-sitemap-example

iamvishnusankar/next-sitemapの使い方

詳しい使い方はREADMEを参照してください。ここでは簡単な使い方を紹介します。

postbuildを使ってサイトマップを静的に生成する

以下のコマンドでnext-sitemapをインストールします。

npm install --save-dev next-sitemap

Rootディレクトリにnext-sitemap.jsを作成します。

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

module.exports = {
  siteUrl: `https://${process.env.NEXT_PUBLIC_SITE_DOMAIN}`,
  generateRobotsTxt: true, // (optional)
  // ...other options
};

.env.localにデプロイするサイトのドメインを以下のように追加します。

NEXT_PUBLIC_SITE_DOMAIN=example.com

package.jsonpostbuildを追加します。

// package.json
 "scripts": {
    "dev": "next dev",
    "build": "next build",
    "postbuild": "next-sitemap",
    "start": "next start",
  },

npm run buildでプロジェクトをビルドすると、robots.txtとsitemap.xml,sitemap-0.xmlを/public/以下に自動で生成します。サイトマップは生成するページの量に応じて自動で分割されます。

ただし、ここで生成されるサイトマップは以下のようにchangefreqpriorityが固定された値で、lastmodもビルド時の時刻になっています。

<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:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url>
<loc>https://example.com</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
<lastmod>2022-04-22T07:12:55.346Z</lastmod>
</url>
<url>
<loc>https://example.com/posts/dynamic-routing</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
<lastmod>2022-04-22T07:12:55.346Z</lastmod>
</url>
<url>
<loc>https://example.com/posts/hello-world</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
<lastmod>2022-04-22T07:12:55.346Z</lastmod>
</url>
<url>
<loc>https://example.com/posts/preview</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
<lastmod>2022-04-22T07:12:55.346Z</lastmod>
</url>
</urlset>

実際のブログなり、ECサイトなりであれば、トップページと詳細ページで更新頻度が違うでしょうし、更新日時は記事データから取得したいでしょう。

next-sitemapはnext-sitemap.jstransformオプションを使って、サイトマップのurlオプションを書き換えることができます。

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

import { getPostBySlug } from './api';

module.exports = {
  siteUrl: `https://${process.env.NEXT_PUBLIC_SITE_DOMAIN}`,
  generateRobotsTxt: true, // (optional)
  // ...other options
  transform: async (config, path) => {
    const post = getPostBySlug(path, ['title', 'date', 'slug']);
    return {
      loc: `/post/${post.slug}`,
      changefreq: config.changefreq,
      priority: config.priority,
      lastmod: post.date,
    };
  },
};

ただし、ここで問題が2つ発生します。1つはnext-sitemapコマンドはCommonJSを実行することを想定しているため、importを解釈できず、SyntaxError: Cannot use import statement outside a moduleが発生します。

一応、ESMに対応してほしいというIssue(351, 217)を見つけることは出来ましたが、今の所、対応する様子はありません。

api.tsのCommonJSバージョン(api.cjs)を作るという方法もありますが、同じ機能を持った2つのコードをメンテナンスすることになるので、あまり良い方法とは言えません。

もう1つは根本的な問題ですが、postbuildはビルド後にしか実行されないので、サイトマップを更新するにはプロジェクトをビルドする必要があります。今回のように表示したい記事がmdxファイルとしてプロジェクト内にあり、かつ更新の頻度が少ない場合は問題になりません。

しかし、CMSからデータを取得して記事を更新している場合には問題になります。この場合は次に紹介するgetServerSideSitemapを使った方法をオススメします。

getServerSideSitemapを使ってサイトマップを動的に生成する

まずは、postbuildを使った方法で生成したファイルや設定を消します。

rm ./next-sitemap.js public/sitemap*

package.jsonからpostbuldを削除します。

/public/robots.txtは以下のように内容を変更します。

# *
User-agent: *
Allow: /

# Host
Host: https://example.com

# Sitemaps
Sitemap: https://example.com/sitemap.xml

/page/sitemap.xml/index.tsxを作成し、以下のようなコードを書きます。

import { getServerSideSitemap } from 'next-sitemap';
import { ISitemapField } from 'next-sitemap/dist/@types/interface';

import { GetServerSideProps } from 'next';

import { getAllPosts } from '../../lib/api';

export const getServerSideProps: GetServerSideProps = async (ctx) => {
  // Method to source urls from cms
  // const urls = await fetch('https//example.com/api')

  const topPageFields = [
    {
      loc: `https://${process.env.NEXT_PUBLIC_SITE_DOMAIN}`,
      lastmod: new Date().toISOString(),
      changefreq: 'daily',
      priority: 1,
    } as ISitemapField,
  ];

  const posts = getAllPosts(['slug', 'date']);
  const postFields = posts.map((post) => {
    return {
      loc: `https://${process.env.NEXT_PUBLIC_SITE_DOMAIN}/posts/${post.slug}`,
      lastmod: post.date || new Date().toISOString(),
      changefreq: 'weekly',
      priority: 0.7,
    } as ISitemapField;
  });

  const fields = topPageFields.concat(postFields);

  return getServerSideSitemap(ctx, fields);
};

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

このコードで以下のようなサイトマップを生成できます。

<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:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url>
<loc>https://example.com</loc>
<lastmod>2022-04-23T01:03:56.700Z</lastmod>
<changefreq>daily</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://example.com/posts/dynamic-routing</loc>
<lastmod>2020-03-16T05:35:07.322Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/posts/hello-world</loc>
<lastmod>2020-03-16T05:35:07.322Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/posts/preview</loc>
<lastmod>2020-03-16T05:35:07.322Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
</urlset>

ただ、この程度ならわざわざ依存関係を増やしてまでnext-sitemapを使う必要を感じません。次の例ではサイトマップを生成するコードを自作します。

SSRでサイトマップを生成するコードを自作する

ここではSSRを使ってサイトマップを生成するコードを自作します。簡単なブログ等ではこのコードで十分事足ります。

まずは/lib/generateSitemap.tsを作成します。

import { getAllPosts } from './api';

const generateSitemapXml = async () => {
  const topPageFields = [
    {
      path: '',
      lastmod: new Date().toISOString(),
    },
  ];

  const posts = getAllPosts(['slug', 'date']);
  const postFields = posts.map((post) => {
    return {
      path: `/posts/${post.slug}`,
      lastmod: post.date || new Date().toISOString(),
    };
  });

  const fields = topPageFields.concat(postFields);

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

  fields.forEach((post) => {
    const url = new URL(
      post.path,
      `https://${process.env.NEXT_PUBLIC_SITE_DOMAIN}`
    );

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

  xml += `</urlset>`;

  return xml;
};

export default generateSitemapXml;

次に/pages/sitemap.tsxを作成します。

import generateSitemapXml from '../lib/generateSitemap';
import { GetServerSideProps } from 'next';

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

  res.statusCode = 200;
  res.setHeader('Cache-Control', 's-maxage=86400, stale-while-revalidate'); // 24時間のキャッシュ
  res.setHeader('Content-Type', 'text/xml');
  res.end(xml);

  return {
    props: {},
  };
};

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

export default SitemapPage;

以下のようなサイトマップが生成されます

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com</loc>
<lastmod>2022-04-23T01:15:41.583Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/posts/dynamic-routing</loc>
<lastmod>2020-03-16T05:35:07.322Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/posts/hello-world</loc>
<lastmod>2020-03-16T05:35:07.322Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/posts/preview</loc>
<lastmod>2020-03-16T05:35:07.322Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
</urlset>

まとめると、コードを書かずにローカルファイルから生成されるページのサイトマップを作りたいならnext-sitemap、CMSなどの外部データから作りたいならSSRで自作が良いです。

正直に言えば、next-sitemapのサイトマップの分割機能はgetServerSideSitemapでこそ実装してほしかったなと思います。サイトマップを分割したいということは大規模なサイトになっているので、それをビルド時にしか更新できないのは不便です。今後の開発に期待したいです(他力本願)。

ちなみにサイトマップを分割するか否かは50MBを超えるかどうかを基準に考えると良いです。他でもないGoogleがその基準を明示しています。

https://developers.google.com/search/docs/advanced/sitemaps/large-sitemaps

Recent Posts

Next.jsでsitemap.xml生成する

Next.jsでSSRやnext-sitemapを用いてsitemap.xmlを生成する方法を紹介します。

Rust製の爆速端末Alacrittyのインストールと設定方法

Rust製のGPUで高速レンダリングするターミナルエミュレータAlacrittyのインストール方法、tmux、NeoVimでのTrue Color、カラースキーム、Font、デフォルトの端末の設定などの紹介をします。

react-modalを使ってModalコンポーネントを作る

react-modalを使ってModalコンポーネントを作る方法を解説します。モーダルの外側をクリックして閉じる、アニメーションを付ける等の基本的な機能から、CSS-in-JS(Emotion)を使ってタイリングの方法を説明します。

Emotionを使ってGatsbyでtailwindを使う方法

Gatsby & Emotion & tailwindの環境構築、スタイリング方法についてまとめました。twin.macroを使えば、classNameだけでなく、Styled Componentsやcss propでスタイリングできます。

GatsbyのLinter/Formatter, CSS-in-JS環境構築

Gatsbyのインストール方法、プロジェクトの作り方、Linter(ESLint, stylelint)/Formatter(prettier)、CSS-in-JS(styled-components, Emotion)、tailwindの設定方法を説明します。