🦔

Next.js Route Handlers ライブラリなしでRSSフィードを作る

に公開

はじめに

今回は、自分のブログにRSSを導入しようと思います。今はSNSがあるので、わざわざRSSリーダーを使う機会は減ったかもしれませんが、一部の人にとっては意味あるものだと思うので実装してみました。

RSSとは

まずRSS(Rich Site Summary、Really Simple Syndication)とは、Webサイトの新着記事や、更新情報の要約、見出しなどを配信する技術で、XML(eXtensible Markup Language)ベースの文書フォーマットの一種です。RSSリーダーと呼ばれるツールや、RSSに対応したブラウザを使用することで、Webサイトにアクセスしなくても、新着記事や更新情報を自動的に取得して、興味のある記事を簡単に閲覧できます。

実装

ネットやChatGPTに聞いてもfeednode-rssを使ってやりましょうという記事が出てきましたが、ライブラリは全くメンテナンスされていないですし、RSSの仕組み自体はそんなに難しいものだと思ったので、ライブラリは特に使わず実装しました。

RSS生成の手順

Next.jsのRoute Handlersを使って実装します。

  1. 記事データの取得: CMS(私のブログはNotionで管理しているためNotion API)から記事の一覧を取得。Notion APIの扱い方はこの記事では取り扱いません。
  2. RSSのXML生成: RSSの文字列を生成。
  3. API作成: app/rss.xml.route.ts でRSSフィードを作成。
  4. ヘッダーにリンクを追加: RSSのリンクを追加。

コード

app/rss.xml/route.ts
import { Client } from "@notionhq/client";
import { NextResponse } from "next/server";

const notion = new Client({
	auth: process.env.NOTION_TOKEN,
});

const getArticles = async () => {
	const filterObject = {
		property: "status",
		status: {
			equals: "published",
		},
	};

	const response = await notion.databases.query({
		database_id: process.env.NOTION_DATABASE_ID as string,
		sorts: [
			{
				// 公開日の降順に並び替える
				property: "published_at",
				direction: "descending",
			},
		],
		filter: filterObject,
	});
	const posts = response.results;

	const postsProperties = posts.map((post: any) => {
		const title = post.properties.title.title[0]?.plain_text;
		const summary = post.properties.summary.rich_text[0]?.plain_text;
		const slug = post.properties.slug.rich_text[0]?.plain_text;
		const publishedAt = post.properties.published_at.date.start;

		return { title, slug, publishedAt, summary };
	});

	return postsProperties;
};

export async function GET() {
 // 1. Notion APIから記事一覧を取得
	const articles = await getArticles();
	
	// 2. RSSのXML生成
	const rss = `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>br-to DevLog</title>
    <link>${process.env.NEXT_PUBLIC_BASE_URL}</link>
    <description>This blog is br-to's devlog.</description>
		<atom:link href="${process.env.NEXT_PUBLIC_BASE_URL}/rss.xml" rel="self" type="application/rss+xml" />
		<language>ja</language>
    ${articles
			.map(
				({ title, slug, publishedAt, summary }) => `
    <item>
      <title>${title}</title>
      <link>${process.env.NEXT_PUBLIC_BASE_URL}/blog/${slug}</link>
			<guid isPermaLink="true">${process.env.NEXT_PUBLIC_BASE_URL}/blog/${slug}</guid>
      <description>${summary}</description>
      <pubDate>${new Date(publishedAt).toUTCString()}</pubDate>
    </item>`,
			)
			.join("")}
  </channel>
</rss>`;
	
	// 3. API作成
	return new NextResponse(rss, {
		headers: {
			"Content-Type": "text/xml; charset=utf-8",
			"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400",
		},
	});
}

src/components/Header.tsx
import Image from "next/image";
import Link from "next/link";
import styles from "./Header.module.css";

export function Header() {
	return (
		<header className={styles.header}>
			<div className={styles.container}>
				<nav className={styles.nav}>
					  ...
					  // リンクを追加
						<Link href="/rss.xml" className={styles.item}>
							<Image src="/rss.svg" width={20} height={20} alt="RSS" />
						</Link>
					</div>
				</nav>
			</div>
		</header>
	);
}

困ったところ

導入の際、苦戦したところはほとんどないですが、RSSのContent-Typeに関しては少しどうするべきかわからず困りました。

Content-Typeをapplication/rss+xmlにするのがRSSフィードであることを正確に示すものとして推奨されているため、最初こちらで実装しましたが、スマホだとブラウザによって挙動が異なりました。

  • safari: App Storeに飛ばそうとするダイアログを表示
  • chrome: rss.xmlをダウンロードしようとするダイアログが表示

この件は色々探しても詳しい記事が全く出なかったのでContent-Typeをapplication/rss+xmlにすることはやめて、Zennに合わせてtext/xmlに変更しました。こうすればどのブラウザでも問題なく/rss.xmlを表示することができました。

RSSフォーマットの確認

作ったRSSのフォーマットが問題ないかは、https://validator.w3.org/feed/ にレスポンスをそのまま投げて確認しました。

動作検証

導入したものが実際に動いているかどうかは、Feedlyという代表的なRSSリーダーにhttps://www.br-to.dev/rss.xmlを登録して確認しました。

Feedlyは会員登録しないとフィードを確認できなかったので注意してください。

まとめ

以上でライブラリを使わずNext.jsのRoute Handlersを使ってRSSを導入することができました。
ここまで読んでいただき、ありがとうございました。

Discussion