mottox2 blog

GatsbyJSで複数のサイトの情報をまとめたRSS Feedをつくる

devgatsby

自分は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の例から抜粋です。)

  1. options.feeds以下のquery内でgraphqlを実行した結果がserializeにqueryとして渡されます。
  2. serializeではqueryをもとにRSSを組み立てるObjectを配列にして返します。
  3. gatsby buildを実行した際にhtmlに<link rel="alternate" type="application/rss+xml" href="/rss.xml"/>が挿入されます。
gatsby-config.js
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に追加しました。

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にロジックを以下のコードを書きます。

  1. query内でいろんなデータソースからデータを取得
  2. queryの結果を纏めた配列をsort関数で日付順に並び替え
  3. internal.typeにデータのtypeが入っているので、それをもとにrssのitemObjectを作成、serializeの結果としてreturnする。

ちょっと雑な感じで、拡張する際に壊れそうな匂いのするコードですが、この状態でgatsby buildを行い生成されたRSSを見たところ期待する結果になっていました。

gatsby-config.js
        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という統一されたインターフェースでアクセスできるというものがあります。個人的には、「会社のエンジニアが運営するブログを一つのサイトにまとめて採用に使う」などといった方法で使われたりすると面白いなと思っています。

  1. QiitaのRSSを取得するには、URLの最後に/feedを付けると見れるみたいです。タグページでも大丈夫みたいです。

dev

ウェブ技術で縦書きを含む画像を生成したい

ここ最近、Web技術を利用した画像生成に興味があります。本記事では、日本語における表現の一種である縦書きに焦点を当て、Web技術を使った縦書きを含む画像生成方法についての調査をまとめました。 > 現

blog

Netlify Formsで問い合わせフォームを作ったら簡単だった

追記(2022/12/29): 問い合わせに対応する窓口をTwitterに統一したいので、フォームページは削除しました。 当ブログは静的サイトホスティングサービスのNetlifyでホスティングされ

netlify
dev

翻訳でHacktoberfestに参加しました

毎年10月に開催されるHacktoberfestに参加しました。このイベントはOSSへの貢献を行い、期間中に規定数(4つ)の貢献を行った人に特典がプレゼントされるものになっています。 自分はドキュメ

Copyright © 2023 @mottox2 All Rights Reserved.