サイトマップの lastmod を毎ビルド更新すると「更新詐欺」になる
この記事について
Astro の静的サイトで <lastmod> 付きのサイトマップを正確に出す実装メモ。@astrojs/sitemap は <lastmod> を出力しないため自前実装が必要だが、素直に書くとビルド日時が入り「全記事が毎日更新された」と申告することになる。updatedDate ?? pubDate という frontmatter 由来の日付を使い、2日付運用と組み合わせることで、実際の更新に連動したサイトマップを出せるようにした。
実装ステータス: 実装済み
@astrojs/sitemap の盲点
Astro には @astrojs/sitemap という公式インテグレーションがある。astro.config.mjs に数行追加するだけでサイトマップが自動生成されるため、多くの Astro サイトで採用されている。
// astro.config.mjs
import sitemap from '@astrojs/sitemap';
export default defineConfig({
integrations: [sitemap()],
});
ただし、このインテグレーションは <lastmod> を出力しない。生成されたサイトマップは次のような形になる。
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com/articles/foo/</loc>
<!-- lastmod が無い -->
</url>
</urlset>
<lastmod> がない状態は「いつ更新されたか不明」を意味する。Google をはじめとする検索エンジンは <lastmod> を更新シグナルとして使うため、これは「更新情報がゼロ」という状態になる。記事を書き直してもクロールの優先度に反映されない。
自前実装で陥りやすい罠
<lastmod> を出すために src/pages/sitemap.xml.ts として自前実装するのが一般的な対処だが、ここで典型的なミスが発生する。
// ❌ やりがちな実装(NG)
export async function GET(context) {
const posts = await getCollection('articles', ({ data }) => !data.draft);
const today = new Date().toISOString().slice(0, 10); // ビルド日時を入れてしまう
const urls = posts.map((post) => ({
loc: `${context.site.href}${post.id}/`,
lastmod: today, // 全記事が今日更新されたことになる
}));
// ...
}
このコードはビルドを走らせるたびに全記事の <lastmod> をその日の日付に更新する。デプロイのたびに「全記事が今日更新された」と申告することになり、実際には変更がない記事の <lastmod> も毎日バンプする。
検索エンジンは繰り返される虚偽の更新シグナルを学習し、そのサイトの <lastmod> を参考にしなくなることがある。意図せず「更新詐欺」を行っている状態になる。
正しい実装: frontmatter 由来の日付を使う
本来の <lastmod> は「このURLのコンテンツが最後に変更された日付」を表す。Astro であれば frontmatter の updatedDate または pubDate を使うのが正解になる。
// ✅ 正しい実装
export async function GET(context) {
const base = context.site.href.replace(/\/$/, '');
// draft と外部リダイレクト(legacyUrl)は除外
const posts = await getCollection('articles', ({ data }) => !data.draft && !data.legacyUrl);
const articleUrls = posts.map((post) => ({
loc: `${base}/${post.id}/`,
lastmod: (post.data.updatedDate ?? post.data.pubDate).toISOString().slice(0, 10),
}));
const staticUrls = [
{ loc: `${base}/` },
{ loc: `${base}/about/` },
];
const urls = [...staticUrls, ...articleUrls];
const body = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls.map((u) =>
` <url><loc>${u.loc}</loc>${u.lastmod ? `<lastmod>${u.lastmod}</lastmod>` : ''}</url>`
).join('\n')}
</urlset>`;
return new Response(body, {
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
});
}
updatedDate ?? pubDate は「更新日があればそれを、なければ公開日を使う」という意味になる。記事を書き直したときだけ updatedDate を frontmatter に追記することで、サイトマップの <lastmod> が実際の更新に連動する。
2日付運用との組み合わせ
この実装が機能するためには、pubDate と updatedDate を別々の概念として運用することが前提になる。
---
title: "記事タイトル"
pubDate: 2026-06-01 # 最初に公開した日。固定。後から変えない
updatedDate: 2026-07-15 # 内容を変更したときだけ追記
---
pubDate: 記事を最初に公開した日付。固定。後から変更しないupdatedDate: 記事の内容を変更した日付。編集したときだけ追記
WordPress の一部プラグインは投稿の再保存で pubDate を更新日で上書きするが、それをやると「古い記事として公開されている」という性質が消える。pubDate は作成日のまま固定し、変更は updatedDate に別記する。
コンテンツを変更していないのに updatedDate を追記するのも誤りになる。「記事を再デプロイした」「typo を直した」「サイトのレイアウトを変えた」などはいずれも updatedDate を更新する正当な理由にならない。コンテンツの実質的な変更があったときだけ追記する。
サイトマップから除外すべきもの
カスタム実装のついでに整理しておくべき点がある。
draft: true の記事: 未公開コンテンツはサイトマップに含めない。URL が実際に存在しないか、noindex になっているため。
外部リダイレクトを持つ記事(legacyUrl): 旧URLが別の場所に移設されて 301 リダイレクトが設定されている場合、旧 URL はサイトマップから除外する。リダイレクト先の URL がインデックスされるため、旧 URL を含めると重複コンテンツとして扱われる可能性がある。
// draft と外部リダイレクトを両方除外
const posts = await getCollection('articles', ({ data }) => !data.draft && !data.legacyUrl);
確認方法
デプロイ後にサイトマップを確認する方法は2つある。
https://your-domain.com/sitemap.xmlに直接アクセスして<lastmod>が出ているか確認する- Google Search Console の「サイトマップ」から取り込み結果を確認する
GSC には <lastmod> を受け取ったかどうかの直接の確認画面はないが、URL の「最終クロール日」が実際の更新日に近い値になっていれば機能している目安になる。
動作環境: Astro 5.x
更新履歴: 2026-07-23 — 初稿
訂正履歴: なし