背景
- Next.jsでも sitemap.xmlを生成したい(WordPress等ならプラグインでいけそう)
- Next.jsでもプラグインがあるが、こんなことのために依存関係を増やすことはあまりしたくない(個人プロジェクトだと尚更)
方法
src/pages/sitemap.xml
に以下を記載
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は以下のように管理されてることとします。
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
を作成し以下のように記述
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;
結果
無駄な依存関係を作らないので嬉しい