Next.jsで静的にRSSフィードを配信

Feedlyで配信されている様子
みなさんはRSSフィード、使ってますか?
僕は使ってないです。ブックマークから気が向いた時に訪問してまとめて読む派なので……。
さて、需要はともかくとして、ブログのRSS対応はたしなみ。仕組みにも関心があったので、このブログでも配信することにした。

調べてみたところ、Next.jsでRSSフィードに対応する方法は以下の2つの選択肢が有力そう。
  • pages/rss.tsxでRSSフィードのxmlを吐き出す
  • publicディレクトリにrss.xmlを直置きする
前者は動的にRSSを生成するので、CMSを外部サービスに依存していたとしても常に更新のフィードを得られるメリットがある。
このブログでは外部CMSは使っておらず、記事投稿とビルドがセットであるため動的である必要はないし、せっかく全てのページをSSGしているので、RSSも静的に配信したい。
よって、後者のpublicディレクトリにrss.xmlを直置きする方法を選択することにした。
とはいえ、記事を追加する度に手動でpublic/rss.xmlも更新してpushするのはつらいので、ビルド時に記事一覧ページをSSGするタイミングでpublicディレクトリにrss.xmlを書き出すという作戦でいくことにした。

XMLの生成

まずはどのようなRSSを生成するかだが、rss.xmlの生成はrssというライブラリ任せにしている。
RSSフィードに必要な情報は記事一覧を生成する時と同じ記事データオブジェクトにあるもので足りるので、特に新たな情報を追加することなくrss.item()を使ってフィードを作っていける。
// utils/feed.ts
function generateEntryRssXml(entries: Entry[], path: string) {
  const rss = new RSS({
    title: SITE_NAME,
    description: SITE_DESCRIPTION,
    site_url: APP_ROOT,
    feed_url: join(APP_ROOT, path, "rss.xml"),
  });

  entries.forEach((entry) => {
    rss.item({
      title: entry.title || entry.slug,
      description: entry.description,
      custom_elements: [
        {
          "content:encoded": {
            _cdata: entry.contentSource,
          },
        },
      ],
      date: new Date(entry.date),
      url: `${APP_ROOT}/entry/${entry.slug}`,
    });
  });

  return rss.xml();
}
全文配信もしたいので、custom_elementsに項目を追加しておいた。
custom_elements: [
  {
    "content:encoded": {
    _cdata: entry.contentSource,
    },
  },
],

XMLのpublic書き出し

生成したxmlをfsでpublicに書き出す。
// utils/feed.ts
export const publishRssXml = async (entries: Entry[], path: string) => {
  const rssDir = join(PUBLIC_PATH, path);
  const rssPath = join(rssDir, "rss.xml");
  const rss = generateEntryRssXml(entries, path);
  if (!fs.existsSync(rssDir)) {
    mkdirp.sync(rssDir);
  }
  fs.writeFileSync(rssPath, rss);
};
第二引数にpathを受け取っているのは、このブログではトップの記事一覧のRSS https://kawamt.com/rss.xml だけでなく、タグごとのRSSも配信しているためだ。
例えば、devタグの記事一覧のページ https://kawamt.com/entry/tags/dev のSSGでは、pathとして/entry/tags/devが指定され、publicに同階層のRSSファイルが生成される。
RSSファイルが3つ生成されている
RSSファイルが3つ生成されている
ちなみに、指定ディレクトリが存在しないとwriteFileSyncで書き出す際に失敗してしまうので、あらかじめmkdirp等を使ってディレクトリ階層を作成しておく必要がある。
if (!fs.existsSync(rssDir)) {
  mkdirp.sync(rssDir);
}

ビルド時に書き出し指示

あとはRSSを配信したい各SSGセクションでpublishRssXml()を走らせてやればよい。
トップの記事一覧
// pages/index.tsx
export const getStaticProps: GetStaticProps = async () => {
  const { entries } = await getEntries();
  publishRssXml(entries, "/");

  return { props: { entries } };
};
タグ付きの記事一覧
// pages/entry/tags/[tag]/index.tsx
export const getStaticProps: GetStaticProps = async ({ params }) => {
  const tag = params?.tag as string;
  const { entries } = await getEntries(tag);
  const path = `/entry/tags/${tag}`;
  publishRssXml(entries, path);

  return { props: { tag, path, entries } };
};
このようにすれば、タグを後から追加しても問題なくRSSも一緒に自動で増えてくれる。Dynamic Routesが上手くハマると気持ちがいい。
個人ブログにおいてタグごとのRSSが必要なのかという疑問はあるが、最近の求人情報ウェブサイトではブログのRSSを利用してポートフォリオを生成するようなサービスを展開していたりするので、全く意味がない訳ではない。
まあ自己満足レベルな実装だとは思うので、そのうちタグごとのRSSは消すかもしれない。