Astro 公式の @astrojs/sitemap は便利ですが、Content Collections の MDX frontmatter にある updatedDate を読んでくれません。
そのままだと、生成される sitemap.xml の各エントリの lastmod がビルド時刻になってしまいます。検索エンジンには「全記事が毎ビルド更新されている」と見え、鮮度シグナルが壊れます。
この記事では、astro.config.mjs の中で MDX を直接走査して updatedDate を抽出し、@astrojs/sitemap の serialize で各 URL に流し込む実装を、最小構成で紹介します。
全体像
2 つの作業をまとめてやります。
-
MDX から lastmod マップを作る:
astro.config.mjsの top-level で MDX を fs.readdir して、パスごとにupdatedDate ?? pubDateを持つ Map を作る -
sitemap に流す:
@astrojs/sitemapのserializeでマップを lookup してitem.lastmodに詰める -
paginated noindex を除外:
filterで/blog/<cat>/<N>/のような noindex ページを sitemap から落とす
最終的な astro.config.mjs の構成は次の通りです。
ステップ 1: lastmod マップを作る
astro.config.mjs の中に async 関数を置き、結果を top-level await で blogLastmod 定数に入れます。
import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
async function buildBlogLastmodMap() {
const map = new Map();
for (const lang of ["en", "ja"]) {
const dir = join(process.cwd(), "src", "content", "blog", lang);
let files = [];
try {
files = await readdir(dir);
} catch {
continue;
}
for (const file of files) {
if (!file.endsWith(".mdx") && !file.endsWith(".md")) continue;
const slug = file.replace(/\.(mdx|md)$/, "");
const raw = await readFile(join(dir, file), "utf8");
const fm = /^---\n([\s\S]*?)\n---/.exec(raw);
if (!fm) continue;
const front = fm[1];
if (/^draft:\s*true/m.test(front)) continue; // 下書きは除外
const updated = /^updatedDate:\s*(\S+)/m.exec(front);
const pub = /^pubDate:\s*(\S+)/m.exec(front);
const dateStr = (updated && updated[1]) || (pub && pub[1]);
if (!dateStr) continue;
const d = new Date(dateStr);
if (Number.isNaN(d.getTime())) continue;
const path = lang === "ja" ? `/ja/blog/${slug}/` : `/blog/${slug}/`;
map.set(path, d.toISOString());
}
}
return map;
}
const blogLastmod = await buildBlogLastmodMap();
getCollection("blog") を使わずに fs.readdir + regex で読んでいるのは、astro.config.mjs は Content Collections の loader より先に評価される ためです。config 評価時は API が初期化されていません。
frontmatter で必要なのは updatedDate と pubDate だけなので、軽量 regex で十分です。
ステップ 2: serialize で sitemap に流す
@astrojs/sitemap の serialize オプションで、各 URL エントリにマップから lookup した lastmod を入れます。
import sitemap from "@astrojs/sitemap";
export default defineConfig({
// ...
integrations: [
sitemap({
i18n: {
defaultLocale: "en",
locales: { en: "en", ja: "ja" },
},
serialize(item) {
const url = new URL(item.url);
// /ja/ プレフィクスを剥がして path 種別で分岐
const pathname = url.pathname.replace(/^\/ja\//, "/").replace(/^\/ja$/, "/");
if (pathname === "/") {
item.changefreq = "daily";
item.priority = 1.0;
} else if (pathname.startsWith("/blog/")) {
item.changefreq = "monthly";
item.priority = 0.7;
// lookup には元の url.pathname(/ja/ 含む)を使う
const lastmod = blogLastmod.get(url.pathname);
if (lastmod) item.lastmod = lastmod;
} else {
item.changefreq = "monthly";
item.priority = 0.5;
}
return item;
},
}),
],
});
changefreq / priority を一緒にセットしているのは、パス種別ごとに整合の取れた値を出すためです。priority は Google が「無視している」と公言していますが、Bing と AI クローラーは引き続き読みに来るので、揃えておく方針です。
ステップ 3: paginated noindex を filter で除外
/blog/build/2/ /blog/reviews/3/ のような 2 ページ目以降は <meta name="robots" content="noindex, follow"> を返している、という構成になっているサイトはよくあります。
このとき、sitemap で 2 ページ目以降を送ってしまうと mixed signal になります。「sitemap に載せた = インデックスしてほしい」と「meta robots = noindex」が同時に立つので、Google / Bing 双方で SEO の品質シグナルとして減点される可能性があります。
sitemap({
filter: (page) => {
if (page.endsWith("/404/") || page.endsWith("/404")) return false;
if (/\/blog\/(build|reviews)\/\d+\/?$/.test(new URL(page).pathname)) return false;
return true;
},
// ...
});
paginated を noindex にする運用と sitemap 除外は、セットで考える必要があります。
落とし穴
実装で踏みかけたところをまとめます。
-
top-level await:
astro.config.mjsでawaitするには ESM 評価が前提です。Astro 5 は標準で OK ですが、.cjs構成では動きません -
draft: true除外: lastmod Map 構築段階で draft をスキップしないと、下書き URL がサイトマップに乗ります -
regex の緩さ:
/^updatedDate:\s*(\S+)/mは単純行を想定しています。YAML 引用符"..."を使うときは(\S+)が"2026-05-25"を丸ごと捕捉するので、new Date()がパースできるかは要確認 -
言語別フォルダの統合:
enとjaを別ループで走らせて 1 つの Map に統合します。キーは/blog/<slug>/と/ja/blog/<slug>/を別物として持つ -
updatedDateの運用: 実装を整えても、注記追加レベルの編集でupdatedDateを打ち替えると信頼度が落ちます。「実質改稿のときだけ更新」ルールとセットで運用するのが本筋です
実運用での updatedDate の更新基準、Zod schema 側との連携、汎用化して @astrojs/sitemap に PR を出すかの判断、Aulvem 本家にまとめています → sitemap の lastmod を MDX frontmatter から自分で差し込む — Aulvem の Astro 公式統合カスタマイズ