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.json
にpostbuild
を追加します。
// 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/
以下に自動で生成します。サイトマップは生成するページの量に応じて自動で分割されます。
ただし、ここで生成されるサイトマップは以下のようにchangefreq
やpriority
が固定された値で、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.js
のtransform
オプションを使って、サイトマップの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