no7.space
😺

Next.js のサイトに RSS フィードを追加する

一時期に比べるとだいぶ下火になった感じのする RSS フィードですが、やっぱり外部サイト連携とか考えると、あったほうが便利だったりもします。
ということで今回は RSS フィードをつけてみます。

RSS について

本筋からは離れるので詳しくは書かないですが、RSS は、ニュースサイトやブログなどの更新情報を配信するためのフォーマットのことです。
RSSリーダーと呼ばれるソフトに読み込ませることで、利用者に更新情報を送るのに使うほか、外部サイトとの連携に使われたりもします。

一時期は、ブラウザにも標準で入ってたり、様々なデベロッパーが様々なRSSリーダーを開発・提供していましたが、今はだいぶ落ち着いた印象もあります。

個人的には Google Reader + Reeder (現 Reeder Classic)というのが好きな組み合わせでした。

Note

Reeder は今でこそ RSS リーダーアプリですが、当時は Google Reader のクライアントアプリで、Google Reader 終了後はソレに依存しない RSS リーダーとして開発が続いているアプリです。

Next.js で RSS フィードをつくるには

RSSは仕様が決まってるのでそれにあわせて頑張ってXML生成すればよさげですが、そんな面倒なことはライブラリに任せるべきだろう、ということで、ライブラリに頼ります。
検索してみたなかで出てきたのは rss と、 feed の2つでした。
ほかもあるかもしれませんが、いずれかの話題なことが圧倒的に多かったです。

両方とも直球な名前ですね。
とりあえず、執筆時点での比較はこんな感じでした。

項目rssfeed
リポジトリdylang/node-rssjpmonette/feed
スター1k1.3k
フォーク131208
Weekly downloads81,225452,907
Last publish9年前2ヶ月前

ダウンロード数みても、最終更新日みても、これは feed のほうが良さそうな気がしますね。

一応 npm trends で比較したりもしてみましたが、やっぱり feed が圧勝っぽい感じするので、今回は feed で行こうと思います。

feed を作る

まずは、パッケージダウンロードしてきます。

ni feed
  • 説明を見ると、new Feed() して初期化時にサイトの基本情報を設定
  • 投稿データをループでまわしながら addItem() で記事情報を追加
  • ほしいフォーマットで出力させる
    • RSS2.0 なら rss2()
    • Atom1.0 だったら atom1()
    • JSON Feed 1.0 なら json1()

記事全体の取得は、記事一覧 作るのに生成してるのでそのロジックをそのまま使うのが良さそうです。

feed 用のルートを作る

正直、XMLを返却するルートは、どこに設置するのが正しいのか、よくわからんのですが、copilot 曰く、

一応こいつも広い意味では API だって、copilot も言ってたので。

Important

更に調べてみたところ、apiを含むルートは Vercel だと Edge 関数になるっぽいのですが、フィードって投稿が更新されなければ中身もかわらないし、ビルド時の内容のままでいいわけだから、これも静的生成で良かったな、ってことで変更しました。

以下にファイルを作成します。

app/feed/route.ts

中身はこうしました

import { Feed } from 'feed'
import { type NextRequest, NextResponse } from 'next/server'
import { getAllPostsMetadata } from '@/lib/posts'
import * as cfg from '@/site.config'

export const dynamic = 'force-static'

export async function GET(request: NextRequest) {
  const { searchParams } = request.nextUrl
  const feedType = searchParams.get('type') || 'rss'
  const postType = searchParams.get('postType') || 'blog'

  const feed = new Feed({
    copyright: cfg.copyright,
    title: cfg.siteTitle,
    description: cfg.siteDescription,
    id: cfg.baseUrl,
    link: cfg.baseUrl,
    generator: '',
  })

  const posts = await getAllPostsMetadata({ postType: postType })

  const FEED_POST_LIMIT = 100

  for (const post of posts.slice(0, FEED_POST_LIMIT)) {
    feed.addItem({
      title: post.title,
      id: `${cfg.baseUrl}/${postType}/${post.ulid}`,
      link: `${cfg.baseUrl}/${postType}/${post.ulid}`,
      description: post.description,
      date: new Date(post.created),
    })
  }

  let xml: string
  let contentType: string

  if (feedType === 'atom') {
    xml = feed.atom1()
    contentType = 'application/atom+xml; charset=UTF-8'
  } else {
    xml = feed.rss2()
    contentType = 'application/rss+xml; charset=UTF-8'
  }

  return new NextResponse(xml, {
    headers: {
      'Content-Type': contentType,
      'Cache-Control': 'public, max-age=3600', // 1 hour cache
    },
  })
}

Feed にのせる内容ってほぼほぼ記事一覧と一緒だったので、一覧作った時に使った記事リスト生成のロジックをそのまま流用しました。

いまのままだと際限なくフィードに書き込まれちゃうので、最大100件くらいまでに絞ることにしました。(もっと少なくてもいいのかな?いくつぐらいがいいのかよくわからず…)

本文を配信するかどうかはまだ悩んでますが、配信するならば一覧取得のロジックも変えねば……

feed のリンクを head に追加する

最後に、サイトの head から feed へのパスを追加して、RSSリーダーやRSSを使って連携してるサイトが、フィードのURLを確認できるようにします。
そのためには、こんな感じで、headlink 要素を追加します

<link rel="alternate" type="application/rss+xml" href="/path/to/feed"/>

で、これを Next でやるには、メタデータの生成のときに使った、 export const metadata をつかいます。
layout.tsx に既にサイトのタイトルとかをいれるのに追加済なので、そこに追記します。

export const metadata: Metadata = {
  title: siteTitle,
  description: siteDescription,
+ alternates: {
+   types: {
+     'application/rss+xml': '/feed',
+   }
+ }
}

リロードしてみて、上記の link 要素が追加されていればOK。
あとは、試しにデプロイしてみて、RSS リーダーに、サイトのトップページなど、feed のURL 以外のURLを入れてみて、
そこから自動的に feed の URL を検知して登録できれば成功です。

さて、これでブログとしての最低限の機能(一覧・詳細・RSS)は整いました。
次に作るべきは、タグでの絞り込みか、検索かなぁ。