RSSフィード自動生成 — feed パッケージでブログをRSS対応にする

Next.js の静的ブログに RSS フィードを追加するのは意外に簡単で、feed パッケージと postbuild スクリプトを組み合わせれば ビルドのたびに自動で feed.xml が生成される。本記事ではこのブログ(Maemaemae)での実装をそのまま解説する。

なぜ RSS フィードが必要か

  • RSS リーダーで購読できる: Feedly、NetNewsWire、Reeder などのユーザーが新記事を見逃さない
  • SEO 的な恩恵: Google が RSS を認識してクロール頻度を上げることがある
  • 他サービスへの連携: Zapier や IFTTT で RSS をトリガーにした自動投稿が可能
  • ヘッドレス CMS 的な活用: RSS を API 代わりに使うサービスがある

静的サイトはサーバーサイドのエンドポイントを作れないため、ビルド時に静的ファイルとして生成しておくのが唯一の選択肢だ。

技術スタック

用途パッケージ
RSS/Atom/JSON Feed 生成feed
Markdown フロントマター解析gray-matter
スクリプト実行タイミング制御npm の postbuild フック

セットアップ

パッケージのインストール

npm install feed
# gray-matter はすでにインストール済みの想定

ディレクトリ構成

blog/
├── posts/          # Markdown 記事
├── tool/
│   └── generate-feed.mjs   # フィード生成スクリプト
├── out/            # next build の出力先
│   └── feed.xml    # 生成されるフィード
└── package.json

スクリプトの実装

tool/generate-feed.mjs の全文を解説する。

import fs from "fs";
import path from "path";
import matter from "gray-matter";
import { Feed } from "feed";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.join(__dirname, "..");
const SITE_URL = "https://maemaemae.blog";

ESM(.mjs)で書いているため __dirname が使えない。fileURLToPath(import.meta.url) で代替する。

記事データの取得

function getPostData() {
  const postsDirectory = path.join(ROOT, "posts");
  const filenames = fs.readdirSync(postsDirectory);

  return filenames
    .map((filename) => {
      const filePath = path.join(postsDirectory, filename);
      const fileContents = fs.readFileSync(filePath, "utf-8");
      const { data } = matter(fileContents);

      return {
        slug: filename.replace(/\.md$/, ""),
        title: data.title,
        description: data.description || "",
        date: data.date,
        image: data.image || null,
        tags: data.tags || [],
      };
    })
    .sort((a, b) => (new Date(a.date) > new Date(b.date) ? -1 : 1));
}

gray-matter でフロントマターだけを抽出している。本文は RSS フィードに含めないためパースしない(高速化のため)。日付の降順ソートは updated フィールド(フィードの最終更新日)に最新記事の日付を使うために必要。

フィードの生成

function generateFeed() {
  const posts = getPostData();

  const feed = new Feed({
    title: "Maemaemae",
    description: "Maemaemae のブログ",
    id: SITE_URL + "/",
    link: SITE_URL + "/",
    language: "ja",
    favicon: SITE_URL + "/favicon.ico",
    copyright: `All rights reserved ${new Date().getFullYear()}, Maemaemae`,
    updated: posts.length > 0 ? new Date(posts[0].date) : new Date(),
    feedLinks: {
      rss2: SITE_URL + "/feed.xml",
    },
  });

  posts.forEach((post) => {
    const url = `${SITE_URL}/posts/${post.slug}`;
    feed.addItem({
      title: post.title,
      id: url,
      link: url,
      description: post.description,
      date: new Date(post.date),
      image: post.image ? SITE_URL + post.image : undefined,
      category: post.tags.map((tag) => ({ name: tag })),
    });
  });

  const outDir = path.join(ROOT, "out");
  if (!fs.existsSync(outDir)) {
    fs.mkdirSync(outDir, { recursive: true });
  }

  fs.writeFileSync(path.join(outDir, "feed.xml"), feed.rss2());
  console.log("feed.xml generated successfully.");
}

generateFeed();

ポイント:

  • idlink はどちらも URL を指定する(RSS 2.0 仕様上の <guid> に使われる)
  • image は絶対 URL でなければならない。/images/xxx.jpg のような相対パスはサイト URL を頭に付けて変換する
  • category{ name: string } の配列形式

postbuild への組み込み

// package.json
{
  "scripts": {
    "build": "next build",
    "postbuild": "next-sitemap && node tool/generate-feed.mjs"
  }
}

npm は build スクリプトの実行後に自動的に postbuild を実行する。next buildnext-sitemapgenerate-feed.mjs の順で動く。

生成される feed.xml の構造

<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" ...>
  <channel>
    <title>Maemaemae</title>
    <link>https://maemaemae.blog/</link>
    <description>Maemaemae のブログ</description>
    <language>ja</language>
    <lastBuildDate>Fri, 07 Mar 2026 00:00:00 GMT</lastBuildDate>
    <item>
      <title>RSSフィード自動生成 ...</title>
      <link>https://maemaemae.blog/posts/blog_20260307</link>
      <guid>https://maemaemae.blog/posts/blog_20260307</guid>
      <description>Next.js 静的ブログに ...</description>
      <pubDate>Fri, 07 Mar 2026 00:00:00 GMT</pubDate>
      <category>Next.js</category>
      <category>Node.js</category>
    </item>
    ...
  </channel>
</rss>

HTML への feed.xml リンク追加

RSS リーダーがフィードを自動検出できるように、<head><link> タグを追加する。

// src/app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ja">
      <head>
        <link
          rel="alternate"
          type="application/rss+xml"
          title="Maemaemae RSS Feed"
          href="/feed.xml"
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

これにより、ブラウザや RSS リーダーがページを開いたときにフィードを自動検出できる。

ハマりやすいポイント

1. .mjs での __dirname が使えない

CommonJS の __dirname は ESM では未定義。以下で代替する。

import { fileURLToPath } from "url";
import path from "path";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

Node.js 20.11.0 以降では import.meta.dirname が直接使えるため、上記の変換は不要になっている。

// Node.js 20.11.0+
const ROOT = path.join(import.meta.dirname, "..");

2. 画像 URL は必ず絶対 URL に変換する

/images/ogp.jpg のような相対パスをそのまま image に渡すと、RSS リーダーが画像を取得できない。

// NG
image: post.image  // "/images/ogp.jpg"

// OK
image: post.image ? SITE_URL + post.image : undefined

3. out/ ディレクトリが存在しない場合がある

next build が先に実行されていれば out/ は存在するが、スクリプトを単体で実行するときに存在しない場合がある。fs.mkdirSync(outDir, { recursive: true }) でガードしておく。

4. date の型変換を忘れない

フロントマターの date"2026-03-07" という文字列で取得される。feed.addItemdate プロパティは Date 型を要求するため new Date(post.date) で変換する。

5. postbuildnpm run build でしか動かない

postbuild は npm の lifecycle スクリプトなので node tool/generate-feed.mjs を直接呼んでもフックは起動しない。開発中に単体テストしたい場合は直接 node tool/generate-feed.mjs を実行する(out/ に書き出されることに注意)。

動作確認

npm run build
# → next build
# → next-sitemap
# → node tool/generate-feed.mjs
# → "feed.xml generated successfully."

ls out/feed.xml  # ファイルが生成されていることを確認

ローカルで内容を確認するには W3C の RSS バリデーターか、Feedly の「フィードを追加」に file:///path/to/out/feed.xml を試してみるとよい(ローカルファイルは多くのリーダーで動作しないためデプロイ後に確認する)。

関連ツールとの比較

Next.js ブログで RSS を生成する方法は複数あるため、選択肢を整理しておく。

手法パッケージ / API特徴
静的ファイル生成(今回)feed + postbuildoutput: "export" の静的サイトに唯一対応。ビルド時のみ実行
Route Handler(App Router)feed + route.tsサーバー機能が使える場合に有効。/feed.xml にリクエストが来るたびに生成
rss パッケージrssRSS 2.0 のみ対応。最終更新が古く(2019年)、feed パッケージの方が現在は推奨
feed-rs(Rust)Node.js エコシステム外。Remix や Deno での利用向け

静的エクスポート(output: "export")を使っている場合は Route Handler が使えないため、今回のように postbuild スクリプトで out/ に直接書き出す方法が正しい選択だ。

まとめ

ステップやること
1. インストールnpm install feed
2. スクリプト作成tool/generate-feed.mjs で記事一覧→フィード変換
3. postbuild 登録package.jsonpostbuildnode tool/generate-feed.mjs を追加
4. head タグ追加layout.tsx<link rel="alternate" ...> を追加
5. デプロイ確認/feed.xml にアクセスして XML が返ることを確認

feed パッケージは RSS 2.0 以外に Atom 1.0・JSON Feed にも対応しているため、feed.atom1()feed.json1() に差し替えるだけで複数フォーマットの同時生成も可能だ。