AstroでRSSとサイトマップを正しく出す
このサイトのリニューアルで、RSSとサイトマップは「ついでに直すもの」の筆頭でした。旧サイト時代のものが残ってはいたのですが、記事のURL体系が変わるタイミングでそのままにしておくと、フィードだけ古いURLを配り続けたり、サイトマップに存在しないページが載ったりします。表示には出てこない部分なので、後回しにすると静かに壊れるんですよね。
Astroではどちらも公式の仕組みが用意されています。RSSは @astrojs/rss、サイトマップは @astrojs/sitemap。自前でXMLを組み立てる必要はないのですが、前提の設定を1つ外すと生成自体が止まったり、間違ったURLで出続けたりします。この記事では、導入手順と、ハマりどころを先に潰すための設定をまとめます。
前提は astro.config の site だけ
両方に共通する前提が、astro.config.mjs の site です。RSSの各記事リンクも、サイトマップのURLも、ここで指定した絶対URLを土台に組み立てられます。
// astro.config.mjs
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://pastora.jp',
integrations: [sitemap()],
});
注意
siteはhttps://から始まる本番URLを書きます。プレビュー環境のURL(*.pages.devなど)をうっかり本番の設定に混ぜると、フィードもサイトマップもプレビュー側のURLで配布されてしまいます。環境ごとに設定ファイルが分かれる構成なら、本番用に正しい値が入っているかを先に確認してください。
site が未設定だと、サイトマップ連携は警告を出して生成をスキップします。ビルド自体は成功するので、「ビルドは通るのにサイトマップだけ無い」という分かりにくい状態になります。
RSS:エンドポイント1本で出す
導入は次の3ステップです。
@astrojs/rssをインストールする(npm install @astrojs/rss。これは連携ではなくヘルパーライブラリなので、integrationsへの追加は不要)src/pages/rss.xml.tsを作り、GET関数からrss()の戻り値を返す- 共通レイアウトの
<head>に autodiscovery の<link>タグを入れ、フィードリーダーがサイトURLからフィードを見つけられるようにする
肝心のエンドポイントはこうなります。このサイトの記事スキーマ(title / date / description / slug / draft)に合わせた形です。
// src/pages/rss.xml.ts
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const posts = await getCollection('blog', ({ data }) => !data.draft);
const sorted = posts.sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime(),
);
return rss({
title: 'PASTORA',
description: 'AI・Astro・Cloudflareまわりの学習メモと、個人サイト運用の記録',
site: context.site,
items: sorted.map((post) => ({
title: post.data.title,
description: post.data.description,
pubDate: post.data.date,
link: post.data.slug,
})),
customData: '<language>ja</language>',
});
}
ポイントは3つあります。
- 下書き除外は getCollection のフィルタでやる。第2引数の関数で
draft: trueを落とします。ここを忘れると、公開前の記事タイトルと概要がフィード経由で丸見えになります。一覧ページの出し分けとは別に、フィード側でも必ず弾くのが安全です - 並び替えは自分でやる。
getCollectionの戻り順は日付順とは限りません。フィードリーダーは基本的にpubDateで並べますが、XML内の順序がばらばらだと検証ツールや一部のリーダーで「新着が埋もれる」見え方になります。降順ソートを明示しておく方が無難です linkは記事の実URLと完全一致させる。このサイトは frontmatter のslug(/posts/…/)がそのままURLになる設計なので、それを使っています。コレクションのidから機械的に組み立てる書き方も公式ドキュメントにはありますが、URLの決め方がどちらなのかを混ぜると、リダイレクト先や404へのリンクを配ることになります
autodiscovery のタグはこうです。
<link
rel="alternate"
type="application/rss+xml"
title="PASTORA"
href={new URL('rss.xml', Astro.site)}
/>
pubDate は Date で渡す
pubDate には Date オブジェクトを渡します。@astrojs/rss が RSS 2.0 の形式(RFC 822形式、GMT表記)に変換してくれるので、文字列の整形は自前でやらなくて構いません。
逆に、frontmatter の日付を文字列のまま扱っていると、ここでつまずきます。「2026-04-23」のような文字列をそのまま渡すと、検証で弾かれたり、タイムゾーンの解釈がリーダーごとにずれたりします。対策はスキーマ側で受け止めることです。Content Collections のスキーマで z.coerce.date() を使って Date に寄せておけば、エンドポイント側は安心して getTime() で比較もできます(この話はContent Collectionsの型でfrontmatterの事故を防ぐで扱いました)。
全文配信にするか、要約だけにするか
@astrojs/rss には、記事本文のHTMLを content に入れて全文配信するやり方もあります。公式ドキュメントの方法は、Markdownの body を markdown-it などでレンダリングし、sanitize-html で掃除してから渡す、というものです。やればできます。ただ、このサイトでは要約(title と description のみ)にしました。判断材料はこうです。
| 観点 | 全文配信 | 要約のみ |
|---|---|---|
| 読む側の体験 | リーダー内で完結して快適 | サイトを開くひと手間がかかる |
| 画像・相対リンク | 相対パスの画像や内部リンクはそのままでは正しく表示されない | 影響なし |
| 実装の手間 | レンダリング+サニタイズの追加実装と、その保守が要る | rss() の基本形だけで済む |
| サイトへの来訪 | リーダーで読み切る分、減りやすい | 記事ごとにサイトへ来てもらえる |
個人の技術ブログで、コードブロックや装飾の見え方まで含めて読んでほしい記事が中心なら、要約でサイトに来てもらう方が記事の意図どおりになります。逆に、短い更新通知が中心のサイトなら全文配信の快適さが勝ちます。「どちらが正解」ではなく、記事の形で決める話だと思います。
サイトマップ:生成されるのは sitemap-index.xml
@astrojs/sitemap は npx astro add sitemap で入ります。ビルドすると出力フォルダに次の2つが作られます。
sitemap-index.xml……サイトマップ本体への目次。Search Console に登録するのはこちらsitemap-0.xml……URLの一覧本体。ページ数が増えるとsitemap-1.xmlのように分割される(1ファイル上限はデフォルト45,000件)
よくある勘違い 登録先を探して
sitemap.xmlを開くと404になります。この連携が作るのはsitemap-index.xmlです。robots.txtに書くのもインデックスの方のURLです。User-agent: * Allow: / Sitemap: https://pastora.jp/sitemap-index.xml
生成対象は、静的に作られる全ページです。getStaticPaths() で展開される動的ルート(記事ページなど)も含まれます。一方で、SSRでオンデマンド生成される動的ルートは対象外です。このサイトは全ページ静的なので、その心配はありません。
載せたくないページは filter で外す
デフォルトでは全ページが載るので、検索結果に出す意味のないページは filter で除外します。filter には site を含む完全なURLが渡ってくるので、パスの断片で判定する書き方が実用的です。
export default defineConfig({
site: 'https://pastora.jp',
integrations: [
sitemap({
filter: (page) =>
!page.includes('/search/') && !page.endsWith('/404.html'),
}),
],
});
このサイトで外しているのは、Pagefindで入れたサイト内検索のページと404ページです。ポイントは「ページ側で noindex を付けたものは、サイトマップからも外す」で、片方だけだと検索エンジンへの信号がちぐはぐになります。noindex のページがサイトマップに載っていると、Search Console で「送信されたURLに noindex が付いています」と報告される、あれです。
なお、changefreq や priority もオプションで付けられますが、Google はこの2つを無視すると公式連携のドキュメントにも明記されています。個人ブログでは無理に付けず、デフォルトのままで十分です。
移行時いちばん怖いのは、フィードURLが変わること
サイトマップは移行でURLが変わっても、Search Console に新しい方を登録し直せば済みます。フィードはそうはいきません。
フィードのURLは購読の宛先そのものです RSSリーダーは登録されたフィードURLを定期的に読みに来ます。そのURLが404になると、リーダー側は「更新が止まったサイト」として扱い、購読が静かに途切れます。移行の前後でフィードURL(このサイトでは
/rss.xml)を変えないこと。どうしても変える場合は、旧URLから新URLへ恒久リダイレクト(301)を必ず残してください。
旧サイト側で /feed や /rss など別名でも配っていた場合は、そちらも棚卸ししてリダイレクト対象に入れます。「自分は使っていないから」で消すと、昔から購読してくれている人の分が切れます。本人には見えないところで起きるので、移行チェックリストの中でも優先度は高めです。
つまずきポイント早見表
| 症状 | 原因 | 直し方 |
|---|---|---|
| ビルドは成功するのにサイトマップが無い | site 未設定で連携が生成をスキップした | astro.config に本番URLの site を設定する |
/sitemap.xml が404 | 生成されるのは sitemap-index.xml | 登録・リンク先をインデックスの方に直す |
| RSSに下書き記事が混ざる | getCollection のフィルタ漏れ | !data.draft のフィルタを入れる |
| フィード内の並びが日付順にならない | 取得順のまま出力している | 渡す前に date 降順でソートする |
| RSSのリンクが実URLとずれる | trailingSlash の扱いや id/slug の混用 | 実URLの決め方に合わせ、trailingSlash: "never" のサイトは rss() にも trailingSlash: false を渡す |
| 移行後に購読者側で更新が止まった | フィードURLが変わって404になった | 旧URLを維持するか、301リダイレクトを残す |
公開前の確認リスト
ビルド後に、この順で目視します。
-
dist/rss.xmlがあり、最新記事が先頭に来ている -
dist/rss.xmlに下書き記事のタイトルが含まれていない - フィード内のリンクがすべて本番ドメインで、実際に開ける
-
dist/sitemap-index.xmlとdist/sitemap-0.xmlが生成されている - サイトマップから検索ページと404が除外されている
-
robots.txtのSitemap:がインデックスのURLを指している - 旧サイトで使っていたフィードURLが、新サイトでも開ける(または301で転送される)
RSSまわりは、壊れてもサイトの見た目が変わらないので、移行のどさくさで一番置いていかれやすい部分です。だからこそ、フレームワーク側の生成に乗せて、確認だけをチェックリスト化しておく。この形がいちばん長持ちすると思っています。
参考
- Add an RSS feed | Astro Docs: https://docs.astro.build/en/recipes/rss/
- @astrojs/sitemap | Astro Docs: https://docs.astro.build/en/guides/integrations-guide/sitemap/