【Nuxt3】SSGでサイトマップを生成する【lastmod対応】
公開日:この記事は以下の記事をベースにしています。
【Nuxt3】SSGモードでxmlサイトマップを作成する方法【動的ページ対応】
上記の記事に従えばNuxtのSSGでサイトマップを公開することは可能なのですが、Nuxt Contentを利用してサイトを作るときに問題があります。
- サイトマップに一番肝心なlastmodが適用されない…!
というわけで、今回は記事にlastmodを適用していきます
TL:DR;
完成品を見たい人はこちら
動作を確認している環境
- nuxt v3.12.3
- sitemap v8.0.0
- node-html-parser 6.1.3
- @nuxt/content 2.13.1
完成品
generate-sitemap.ts
import type { Nitro } from "nitropack";
import { SitemapStream, streamToPromise, type SitemapItem } from 'sitemap';
import { Readable } from 'stream';
import { readFileSync, writeFileSync } from 'fs';
import { parse } from 'node-html-parser'
import path from 'path';
import { consola as logger } from 'consola';
import { colors } from 'consola/utils';
// サイトドメインを指定(最後スラッシュ不要)
const domain = process.env.BASE_URL || 'http://localhost:3000';
export default async function generateSitemap(nitro: Nitro) {
if (!nitro._prerenderedRoutes) {
return;
}
logger.info("Generating Sitemap");
const startTime = process.hrtime();
const publicDir = nitro.options.output.publicDir;
const routes = nitro._prerenderedRoutes?.map((e) => e.fileName || null).filter((e, i, a) => e && a.indexOf(e) === i && e.endsWith("index.html")).map((e) => {
let lastmod = undefined
//ブログ記事ページの場合
if (e?.match(/^\/blog\/(?!tags\/)(?!index\.html$).*\.html$/)) {
const html = parse(readFileSync(path.join(publicDir, e), 'utf-8'))
const publishedTime = html.querySelector('meta[property="article:published_time"]')?.getAttribute('content')
const modifiedTime = html.querySelector('meta[property="article:modified_time"]')?.getAttribute('content')
lastmod = (modifiedTime || publishedTime)
}
return {
url: e?.replace(/index\.html$/, '').replace(/\/+$/, ''),
priority: 0.7,
changefreq: 'weekly',
lastmod
} as SitemapItem;
})
const smStream = new SitemapStream({ hostname: domain, lastmodDateOnly: true });
Readable.from(routes).pipe(smStream);
const data = await streamToPromise(smStream);
writeFileSync(path.join(publicDir, 'sitemap.xml'), data.toString());
const endTime = process.hrtime(startTime);
logger.log(`${routes.map((e) => colors.gray(` ├─ ${e.url}`)).sort().join('\n')}`);
logger.info(`Generateed Sitemap ${routes.length} routes in ${(endTime[0] + (endTime[1] / 1e6) / 1000).toFixed(3)} seconds`)
logger.success(`Generated Sitemap at ${path.join(publicDir, 'sitemap.xml')}`);
}
改変元からの差分
diff
--- before.ts
+++ after.ts
@@ -1,32 +1,51 @@
import type { Nitro } from "nitropack";
-import { SitemapStream, streamToPromise, SitemapItem } from 'sitemap';
+import { SitemapStream, streamToPromise, type SitemapItem } from 'sitemap';
import { Readable } from 'stream';
-import { writeFileSync } from 'fs';
+import { readFileSync, writeFileSync } from 'fs';
+import { parse } from 'node-html-parser'
import path from 'path';
+import { consola as logger } from 'consola';
+import { colors } from 'consola/utils';
// サイトドメインを指定(最後スラッシュ不要)
-const domain = "https://YOUR_DOMAIN";
+const domain = process.env.BASE_URL || 'http://localhost:3000';
-export default async function genSitemap(nitro: Nitro) {
- if (!nitro._prerenderedRoutes) {
- return;
+export default async function generateSitemap(nitro: Nitro) {
+ if (!nitro._prerenderedRoutes) {
+ return;
+ }
+
+ logger.info("Generating Sitemap");
+ const startTime = process.hrtime();
+
+ const publicDir = nitro.options.output.publicDir;
+
+ const routes = nitro._prerenderedRoutes?.map((e) => e.fileName || null).filter((e, i, a) => e && a.indexOf(e) === i && e.endsWith("index.html")).map((e) => {
+ let lastmod = undefined
+
+ //ブログ記事ページの場合
+ if (e?.match(/^\/blog\/(?!tags\/)(?!index\.html$).*\.html$/)) {
+ const html = parse(readFileSync(path.join(publicDir, e), 'utf-8'))
+ const publishedTime = html.querySelector('meta[property="article:published_time"]')?.getAttribute('content')
+ const modifiedTime = html.querySelector('meta[property="article:modified_time"]')?.getAttribute('content')
+ lastmod = (modifiedTime || publishedTime)
}
- const publicDir = nitro.options.output.publicDir;
-
- const routes = nitro._prerenderedRoutes?.map((e) => e.fileName || null).filter((e, i, a) => e && a.indexOf(e) === i && e.endsWith("index.html")).map((e) => {
- return {
- url: e?.replace(/index\.html$/, ''),
-
- // この辺の値は各自調整して下さい
- changefreq: 'weekly',
- priority: .7,
- } as SitemapItem;
- });
- const smStream = new SitemapStream({ hostname: domain });
- Readable.from(routes).pipe(smStream);
-
- const data = await streamToPromise(smStream);
-
- writeFileSync(path.join(publicDir, 'sitemap.xml'), data.toString());
+ return {
+ url: e?.replace(/index\.html$/, '').replace(/\/+$/, ''),
+ priority: 0.7,
+ changefreq: 'weekly',
+ lastmod
+ } as SitemapItem;
+ })
+ const smStream = new SitemapStream({ hostname: domain, lastmodDateOnly: true });
+ Readable.from(routes).pipe(smStream);
+
+ const data = await streamToPromise(smStream);
+
+ writeFileSync(path.join(publicDir, 'sitemap.xml'), data.toString());
+ const endTime = process.hrtime(startTime);
+ logger.log(`${routes.map((e) => colors.gray(` ├─ ${e.url}`)).sort().join('\n')}`);
+ logger.info(`Generateed Sitemap ${routes.length} routes in ${(endTime[0] + (endTime[1] / 1e6) / 1000).toFixed(3)} seconds`)
+ logger.success(`Generated Sitemap at ${path.join(publicDir, 'sitemap.xml')}`);
}
主な変更点
- lastmodに対応するようにした
- それに伴ってページを全件クロールしてmetaタグを読みに行ってるので、生成時のログを足して実行時間を表示するようにした
lastmodに対応させたい…
サイトマップで最も大事なのはlastmodの値といっても過言ではありません。
SSG時にその値をどうやって取得しようかな…と悩んだのですが、今回はnode-html-parser
を用いてmetaタグから<meta property="article:published_time">
または<meta property="article:modified_time">
を取得することで解決しています。ブログページ以外には適用しないことで実行時間をなるべく抑えています。
ログをポップに表示する
Nuxtのログ、絵文字が使われていてポップで使いやすいな…と思っていたのですがどうやらconsolaというライブラリを使うことで可能なよう。(ちなみにNuxtを利用している場合はわざわざ導入しなくても入っています)
うーん、めっちゃ公式のログっぽい
さいごに
この内容は(仮)さんの【Nuxt3】SSGモードでxmlサイトマップを作成する方法【動的ページ対応】の内容がベースとなっています。
シェアと改変の許可を頂いた(仮)さんにこの場を借りて感謝いたします。
シェアする?