【Next.js】RSSフィードをサムネイル画像付きで配信する

テクノロジー
2024/01/30
post-hero-image

RSS(Rich Site Summary)とは、Webサイトの更新情報をユーザーに対して通知できるフォーマットです。

Wikipediaによると、1999年の"RSS0.9"がRSSの起源とのことなので、25年ほど前から存在する技術ということですね。*1

「RSSリーダー」と呼ばれる専用ソフトウェアや、RSS対応のブラウザで通知の受け取りが可能です。

いまや「通知」機能といえば、スマホアプリのプッシュ通知が馴染み深いですが、現在でもFeedly, InoreaderなどのRSSリーダーが情報通のビジネスパーソンを中心に根強い指示を集めている印象です。


本記事ではそんな古の技術であるRSSを、【令和最新版】Webアプリ開発フレームワークのNext.jsに搭載する方法をご紹介します。


具体的にはこんな感じでサムネイル画像を付けて配信します。スクショはInoreaderの様子です。



Gatsbyの記事と同じ画像じゃないかって?
そうですよ。trogがGatsbyで搭載した機能のマイグレーションというやつです。

(なんのことかわからない方のための関連ページ: 【Gatsby】RSSフィードをサムネイル画像付きで配信する - trog


それではNext.jsアプリケーションでもRSS配信していきましょう!

準備

前提条件

  • Next.jsアプリケーションが構築済み
  • RSS 2.0準拠


弊ブログtrogではCMSとしてmicroCMSを採用しており、この記事もmicroCMSから記事を投稿しています。

基本的にはNext.jsで構築されたアプリケーション全般で使用できるように説明します。

実装解説の最後に、microCMSから取得した情報のRSSファイルを作成するコードをご紹介します。

使用パッケージ


xmlbuilderのほうが人気がある(らしい)ですが、最新版のxmlbuilder2のほうが使いやすくなっておりオススメです。

xmlbuilder2のメソッド

今回は、xmlbuilder2のXML生成メソッドのうち、.ele(), .txt(), .up()を使用します。

.ele()

.ele(name, attributes, text)メソッドでは、新しいXML要素を作成できます。

  • name:新しい要素の名前です。
  • attributes(オプション):新しい要素の属性を定義するオブジェクトです。
  • text(オプション):新しい要素のテキスト内容です。

例えば、以下のコードは<item isPermaLink="false">Hello</item>というXML要素を作成します。

doc.ele('item', { isPermaLink: "false" }, 'Hello');

.txt()

.txt()メソッドは、XML要素の中に直接テキストを挿入できます。

例えば、以下のコードは<title>要素にHelloというテキストノードを追加します。

doc.ele('title').txt('Hello');


これにより、<title>Hello</title>というXMLが生成できます。

.up()

.up()メソッドは、いまある要素の親要素に移動します。これは、ネストされた要素を作成するときに便利です。

例えば、以下のコードは<channel>要素の中に<title>要素を作成します。

doc.ele('channel').ele('title', {}, 'Hello').up();

このコードは、<channel><title>Hello</title></channel>というXMLを生成します。

.up()メソッドを呼び出すことで、<title>要素の親である<channel>要素に戻ります。
これにより、<channel>要素の中に、さらに子要素を追加できます。

基本のRSS情報

scripts/generate-rss.mjsという名前でファイルを作成します。

普通の*.jsファイルでも問題ないです。*2

RSS情報の核となる部分はこんな構成です。(イメージ)

<item>
  <title>記事のタイトル</title>
  <link>記事のURL</link>
  <pubDate>記事の公開日</pubDate>
  <description>記事の要約</description>
  <guid isPermaLink="false">記事のパス</guid>
  <media:content url="サムネイル画像のURL" width="1920" height="1080" media="image"/>
</item>


titleからdescriptionまでは載せたい情報をそのまま記載してください。
guidは、記事を一意に識別するために必要な情報です。
タイトルにしてしまうと変更する可能性があるので、記事のURLを指定しています。

guidは任意の値を指定できるものの、URLが入ることが前提のようです。*3
isPermaLink 属性をfalseに指定しておくと、guidがURL(フルパス)ではないことをRSSのルールに則って示せるようです。

...が、サイトによってguidに指定している値がバラバラだったり、そのタグ名だけでは「URLのフルパスを指定する」というルールがわかりづらく、周知されていない様子です。
URLはlinkで解釈してもらうことにしています。(trogではguidはお飾りになっています)

核となる<item>の中についてはこのように実装しました。

  articles.forEach((article) => {
    feed.ele('item')
      .ele('title').txt(article.title).up()
      .ele('link').txt(`https://microayatron.com/${article.slug}`).up()
      .ele('pubDate').txt(article.publishedDate).up()
      .ele('description').txt(article.metaDescription).up()
      .ele('guid', { isPermaLink: "false" }).txt(`https://microayatron.com/${article.slug}`).up()
      .ele('media:content', {
        url: article.image?.url,
        width: 1920,
        height: 1080,
        media: 'image',
      }).up();
  });


articlesはmicroCMSから取得した、記事情報のかたまりです。feedはヘッダー定義の紹介時に説明します。

画像情報

Media RSS タグ( <media:content> )には画像のURL、画像の高さ・幅、メディアの種類を指定します。

.ele('media:content', {
  url: article.image?.url,
  width: 1920,
  height: 1080,
  media: 'image',
}).up();


メディアの種類は今回の場合imageと指定してください。
これらの項目を「属性」として指定する必要があるため、オブジェクトにまとめて突っ込みます。

これだけだと”Namespace prefix media on content is not defined.”のエラーが表示されます。
Media RSS タグを使用するために、RSSのヘッダー部分に xmlns:media="http://search.yahoo.com/mrss/"を定義する必要があります。

ヘッダー定義はこのようにしました。

  const feed = create({ version: '1.0', encoding: 'UTF-8' })
    .ele('rss', {
      'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
      'xmlns:content': 'http://purl.org/rss/1.0/modules/content/',
      'xmlns:atom': 'http://www.w3.org/2005/Atom',
      'version': '2.0',
      'xmlns:media': 'http://search.yahoo.com/mrss/'
    })
    .ele('channel')
    .ele('title').txt('trog').up()
    .ele('link').txt('https://microayatron.com/').up()
    .ele('description').txt('RSS feed for microayatron.com').up();

feedという変数を定義して、その中にどんどん項目を突っ込んでいます。
ヘッダーなど外枠の生成が終わったら、さきほど説明したitemの中身を生成します。

RSSファイルを生成するとこうなります。

<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>trog</title>
    <link>https://microayatron.com/</link>
    <item>(先述の記事情報1)</item>
    <item>(記事情報2)</item>
    <item>(記事情報3)</item>
    ...
  </channel>
</rss>


RSS生成

実装

配信用のRSS情報を記載したrss.xmlを生成します。

具体的な生成場所は、/public/feed/rss.xmlとしています。
こうすることでRSS配信URLを、https://hogehoge.com/feed/rss.xmlとすることができます。

実装のまとめとして、microCMSを採用している場合のgenerate-rss.mjsを載せておきます。

import { createClient } from 'microcms-js-sdk';
import fs from 'fs';
import path from 'path';
import { create } from 'xmlbuilder2';

const filePath = path.resolve('public/feed/rss.xml');

// ディレクトリが存在するか確認、存在しない場合は作成する
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)){
    fs.mkdirSync(dir, { recursive: true });
}


// ファイルが存在するか確認、存在しない場合は作成する
if (!fs.existsSync(filePath)) {
    fs.writeFileSync(filePath, '', 'utf8');
}


const client = createClient({
  serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN, 
  apiKey: process.env.MICROCMS_API_KEY, 
});


const generateRssFeed = async () => {
  try {
    // microCMSから記事を取得する
    const response = await client.getList({
      endpoint: 'blog', // 環境に合わせて変更してください
      queries: { limit: 100,  orders: '-publishedDate' } // 公開日時が新しい順
    });

    const articles = response.contents || [];

    // フィードの作成
    const feed = create({ version: '1.0', encoding: 'UTF-8' })
      .ele('rss', {
        'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
        'xmlns:content': 'http://purl.org/rss/1.0/modules/content/',
        'xmlns:atom': 'http://www.w3.org/2005/Atom',
        'version': '2.0',
        'xmlns:media': 'http://search.yahoo.com/mrss/'
      })
      .ele('channel')
      .ele('title').txt('trog').up()
      .ele('link').txt('https://microayatron.com/').up()
      .ele('description').txt('RSS feed for microayatron.com').up();


    // フィードに記事を追加する
    articles.forEach((article) => {
      feed.ele('item')
        .ele('title').txt(article.title).up()
        .ele('link').txt(`https://microayatron.com/${article.slug}`).up()
        .ele('pubDate').txt(article.publishedDate).up()
        .ele('description').txt(article.metaDescription).up()
        .ele('guid', { isPermaLink: "false" }).txt(`https://microayatron.com/${article.slug}`).up()
        .ele('media:content', {
          url: article.image?.url,
          width: 1920,
          height: 1080,
          media: 'image',
        }).up();
    });


    return feed.end({ prettyPrint: true });
  } catch (error) {
    console.error('Error fetching data from MicroCMS:', error);
    throw error;
  }
};

const generateAndSaveRssFeed = async () => {
  let rssFeed = await generateRssFeed();

  if (rssFeed) {
    fs.writeFileSync(filePath, rssFeed);
    console.log('RSS feed generated and saved.');
  }
};

generateAndSaveRssFeed();


RSSファイル生成の流れ

trogではビルド時にRSSファイルを生成するように設定しています。

trogの記事が更新されるまでの流れを簡単にご説明すると...

  1. microCMSから[公開]ボタンを押して記事を公開
  2. Webhookで接続されたNetlify(ホスティングサービス)で公開されたことを検知
  3. microCMSから最新の情報を取得のうえ、Netlifyがtrogをビルド実行
  4. 公開用のページ(HTMLファイル)が生成される


ということで、trogでは自動でサーバーに最新記事がアップロードされる仕組みにしてあります。
(設定すればちゃんと動くようにmicroCMSがあらかじめ用意してくれてるだけ)

先述のgenerate-rss.mjsの実装の中で、microCMSから最新記事を取得するようにしました。
ビルド時にgenerate-rss.mjsを実行することにより、RSS情報に自動で更新した記事が追加される...という流れにしていきます。


* * *


それではRSS生成のトリガーとなる部分を実装していきます。

next.config.jschild_processモジュールを使用して、scripts/generate-rss.mjsスクリプトを同期的に実行するよう設定します。

const { execSync } = require('child_process');

execSync('node scripts/generate-rss.mjs', { stdio: 'inherit' });


package.jsonscript部分に、generate-rssコマンドを記載します。

"scripts": {
    "build": "node build.js && next build",
    "generate-rss": "node scripts/generate-rss.mjs"
  },


これでビルド時に/public/feedrss.xmlが生成されるようになります。

採用しているパッケージやツールに合わせて、お好みで変更してください。

テスト

正しく生成されているかどうか確認していきます。

まずは開発環境でさきほど定義したnpm run buildを実行したのちに、next startを実行します。
http://localhost:3000/public/feed/rss.xmlにアクセスして、RSS情報が確認できれば成功です。

ここで

  • 生成されたRSS情報のヘッダー情報にxmlns:media="http://search.yahoo.com/mrss/"が含まれていること
  • 各記事情報の中に<media:content>が含まれていること

の2点を確認してください。


Chromeで確認する場合は、こんな感じのカラフルな画面になっているかもしれません。


xmlとして誤りがある場合はエラーが表示されます。


よかったらデバッグの参考にしてください。

RSSリーダーから確認(デプロイ後)

アプリケーションを本番環境にデプロイすれば、Feedly, InoreaderなどのRSSリーダーから確認できます。

各RSSリーダーが、ブログから配信されたRSS情報を解釈して、わかりやすいUIに落とし込んでいます。

お好きなRSSリーダーから、本番環境の配信先URL(例: https://hogehoge.com/feed/rss.xml)を試しに購読して、確認してみてください。

おわりに

最近では分散型SNSがユーザーアカウントごとの投稿をRSSを配信しているようです。

それまでRSSを使ったことのなかった世代でも、MiskeyやマストドンなどでRSSに触れる機会が増えることになります。

ご自身のブログでもRSS配信機能を実装すれば、分散型SNSからRSSの存在を知った新参の情報通からのアクセスが期待できるようになるかもしれませんね。


「レターパックで現金送れ」はすべて詐欺です。*4
皆さまくれぐれもお気をつけください。



* * *

1: https://ja.wikipedia.org/wiki/RSS#:~:text=%E6%9C%80%E5%88%9D%E3%81%AERSS%E3%81%A7%E3%81%82%E3%82%8B,%E7%AD%96%E5%AE%9A%E3%81%97%E3%81%9F%E3%82%82%E3%81%AE%E3%81%A7%E3%81%82%E3%82%8B%E3%80%82 - RSS - Wikipedia

2: 仕事ではTypeScriptで開発していますが、trogはTypeScript化をサボっており、いまだにJavaScriptのままです。でもimportは書き慣れたECMAScriptを使いたいため、*.mjsを採用しました。
ちなみにtrogを構成するファイル群の中でmjsはここだけです。ただただ整備をサボっています。

*3: https://www.rssboard.org/rss-draft-1#element-channel-item-guid - RSS 2.0 Specification
https://validator.w3.org/feed/docs/error/InvalidPermalink.html "guid must be a full URL, unless isPermaLink attribute is false" -  Feed Validation Service
https://support.google.com/feedburner/answer/79014?hl=ja 「フィード コンテンツの記事のリンクが機能しないのはなぜですか?」 - FeedBurner ヘルプ

*4: https://dic.pixiv.net/a/%E3%83%AC%E3%82%BF%E3%83%BC%E3%83%91%E3%83%83%E3%82%AF%E3%81%A7%E7%8F%BE%E9%87%91%E9%80%81%E3%82%8C - レターパックで現金送れ (れたーぱっくでげんきんおくれ)とは【ピクシブ百科事典】

    この記事を共有する
とろ(microayatron)
profile-icon

Webアプリケーションのプログラマ(フロントエンドエンジニア) Angular(TypeScript) / Next.js / Cypress を主に使用。
前職はピアノ技術者(調律師)。2017年からブログ「trog」を運営。
あざらしと音楽が好き。

trogではプッシュ通知機能を提供しています。

新しい記事が投稿されたタイミングで、お使いの端末にお知らせが届きます。

よければ通知設定ページから通知の許可をお願いします。