GatsbyJSで複数のサイトの情報をまとめたRSS Feedをつくる
自分はWrite Blog Every Weekという週に一回ブログを書くコミュニティに所属しています。 コミュニティ内ではRSS Feedに基づいてブログを書いたかを判断するBotが整備されており、ブログ管理者がそのBotに対してRSSのURLを登録するフローを取っています。 しかし現状、1人が登録できるRSSは一つまでという制約があり、ブログとQiita、noteを併用して認識させることが難しくなっています。 正攻法で言えばプルリクエストを出そうという話になります。しかし、AWS LambdaやDynamoDBが使われておりデバッグも面倒なので(自分の登録する)RSSに複数のサイトの情報をまとめるという変則的な方法を取って解決することにしました。
その際に、Gatsbyのgatsby-plugin-feedを使って複数のサイトをまとめたのでその方法を紹介したいと思います。
該当のPull Request: https://github.com/mottox2/website/pull/41
gatsby-plugin-feedとは?
gatsby-plugin-feedGraphQLで取得したデータをもとにRSS Feedを作成、metaタグに情報を埋め込む責務をもつプラグインです。gatsbyjs/gatsbyで管理されている公式のプラグインとなっており、gatsby-starter-blogにも使用されていることから利用者の多いプラグインです。
通常の利用法では次のようになります。(以下の例はREADME.mdの例から抜粋です。)
- options.feeds以下のquery内でgraphqlを実行した結果がserializeにqueryとして渡されます。
- serializeではqueryをもとにRSSを組み立てるObjectを配列にして返します。
- この際のObjectはrssモジュールに準じたものにします。
gatsby build
を実行した際にhtmlに<link rel="alternate" type="application/rss+xml" href="/rss.xml"/>
が挿入されます。
plugins: [
{
resolve: `gatsby-plugin-feed`,
options: {
query: `
{
site {
siteMetadata {
title
description
siteUrl
site_url: siteUrl
}
}
}
`,
feeds: [
{
serialize: ({ query: { site, allMarkdownRemark } }) => {
return allMarkdownRemark.edges.map(edge => {
return Object.assign({}, edge.node.frontmatter, {
description: edge.node.excerpt,
date: edge.node.frontmatter.date,
url: site.siteMetadata.siteUrl + edge.node.fields.slug,
guid: site.siteMetadata.siteUrl + edge.node.fields.slug,
custom_elements: [{ "content:encoded": edge.node.html }],
})
})
},
query: `
{
allMarkdownRemark(
limit: 1000,
sort: { order: DESC, fields: [frontmatter___date] },
filter: {frontmatter: { draft: { ne: true } }}
) {
edges {
node {
excerpt
html
fields { slug }
frontmatter {
title
date
}
}
}
}
}
`,
},
],
},
},
]
補足: optionsの初期値にはgatsby-source-filesystem, gatsby-transformer-remarkを使った構成でのものが設定されており、単純な構成の場合optionsが必要ありません。(人のソースを参考にする場合注意してください。)
複数のサイトの情報を混ぜる
上記の例からわかるのは、「GraphQLで得られるデータをもとにRSSを組み立てる」ということです。なので、混ぜ込みたい情報をGatsbyのGraphQLで扱うためにsource-pluginを導入したり、gatsby-node.jsでcreateNode関数を実行します。
今回は自分のnote.mum, qiita.com1のRSSを混ぜ込むことにしました。
データの用意
RSSを混ぜるには gatsby-source-rss-feedを使用します。今回は以下の記述をgatsby-config.js
に追加しました。
module.exports = {
...
plugins: [
...,
{
resolve: `gatsby-source-rss-feed`,
options: {
url: `https://note.mu/mottox2/rss`,
name: `NotePost`
}
},
{
resolve: `gatsby-source-rss-feed`,
options: {
url: `https://qiita.com/mottox2/feed`,
name: `QiitaPost`
}
},
]
}
これでNote, Qiitaの記事をそれぞれallFeedNotePost
, allFeedQiitaPost
というクエリで情報が取得できるようになります。
RSSをつくる
gatsby-config.js
内のgatsby-plugin-feedのoptionsにロジックを以下のコードを書きます。
- query内でいろんなデータソースからデータを取得
- queryの結果を纏めた配列をsort関数で日付順に並び替え
- internal.typeにデータのtypeが入っているので、それをもとにrssのitemObjectを作成、serializeの結果としてreturnする。
ちょっと雑な感じで、拡張する際に壊れそうな匂いのするコードですが、この状態でgatsby build
を行い生成されたRSSを見たところ期待する結果になっていました。
feeds: [
{
serialize: ({ query: { site, allEsaPost, allFeedQiitaPost, allFeedNotePost } }) => {
return [...allEsaPost.edges, ...allFeedNotePost.edges, ...allFeedQiitaPost.edges].sort((a, b) => {
const bDate = b.node.pubDate ? new Date(b.node.pubDate) : new Date(b.node.childPublishedDate.published_on)
const aDate = a.node.pubDate ? new Date(a.node.pubDate) : new Date(a.node.childPublishedDate.published_on)
return bDate - aDate
}).map(edge => {
const node = edge.node
switch (node.internal.type) {
case 'EsaPost':
const day = dayjs(node.childPublishedDate.published_on)
return {
date: day.toISOString(),
pubDate: day.toISOString(),
url: `${site.siteMetadata.siteUrl}/posts/${node.number}`,
guid: node.number,
title: node.fields.title,
description: node.fields.excerpt
}
break;
case 'FeedQiitaPost':
case 'FeedNotePost':
return {
date: dayjs(node.pubDate).toISOString(),
pubDate: dayjs(node.pubDate).toISOString(),
url: node.link,
guid: node.link,
title: node.title,
description: node.contentSnippet.substring(0, 512)
}
break;
default:
throw `${node.internal.type} is unknown type`
}
})
},
query: `
{
allEsaPost {
edges {
node {
number
fields {
title
excerpt
}
childPublishedDate {
published_on
}
internal {
type
}
}
}
}
allFeedQiitaPost {
edges {
node {
title
pubDate
contentSnippet
link
internal {
type
}
}
}
}
allFeedNotePost {
edges {
node {
title
pubDate
contentSnippet
link
internal {
type
}
}
}
}
}
`,
この方法の問題点
以上の方法でRSS Feedを一つにまとめることが出来ました。 しかし、静的サイトという特性上、ビルドのタイミング次第でRSSが更新されないという現象が起こりえます。ブログ記事の更新はWebhookを利用することで更新していますが、RSSの更新をhookするにはIFTTTやZapierなどの方法を別に用意する必要があります。 ただ、自分は厳密なRSSを必要としていないので、無視することにしました。どうしても気になるようであれば、スケジューラーでサイトの更新を行うと思います。
何度もいうと、複数のRSSを受け付ける実装をするのが一番いいです。
おわりに
これで、週一ブログに追われて自分のブログだけを更新する。ということはなくなると思います。 ちょっと薄めの記事はQiita、デザイナー層にリーチしたい記事はnote.mute。といった使い分けをしていけたら…という気持ちです。
また、今回は複数のデータソースをRSSにまとめるという形を取りましたが、これはRSSじゃなくてHTML上にまとめるということも可能です。GatsbyJSの強みには複数のデータソースに対してGraphQLという統一されたインターフェースでアクセスできるというものがあります。個人的には、「会社のエンジニアが運営するブログを一つのサイトにまとめて採用に使う」などといった方法で使われたりすると面白いなと思っています。
注釈
-
QiitaのRSSを取得するには、URLの最後に
/feed
を付けると見れるみたいです。タグページでも大丈夫みたいです。 ↩