Nuxt.js の静的生成で作ったブログに RSS2.0 を生やす方法

こんにちは、Pちゃんです。 
これは [DeNA Advent Calendar 2018](https://qiita.com/advent-calendar/2018/dena) の6日目の記事です。

## TL;DR

- DeNA のデザイン本部が運営する [DeNA DESIGN BLOG](https://design.dena.com/) というブログがある
- 最近のリニューアルで WordPress から Nuxt.js (静的生成) に移行した
- RSSが消失した:innocent:
- [生やした](https://design.dena.com/feed.xml)

## DeNA DESIGN BLOG の仕組みにについて

[DeNA DESIGN BLOG](https://design.dena.com/) は、主に以下のようなスタックで構成されています。

- Nuxt v2.x
- TypeScript
- Markdown

![nuxt-generate.png](https://qiita-image-store.s3.amazonaws.com/0/32100/8efcc8b8-f423-41a2-da04-83719626f978.png)

上の図のとおり、 `$ nuxt generate` をすると

1. 自作スクリプトや processmd を使って、全記事データを1つのJSONファイルに固める
2. 全記事データが入ったJSONファイルを元に、Vueテンプレートに流し込んでトップページや記事ページのHTMLファイルを書き出す

と言った処理が行われます。 Nuxt.js の静的生成でブログを作っているところは、どこもこんな感じの流れなのではないでしょうか？


## @nuxt/feed は静的生成するときは向かない

さて、 Nuxt.js で RSS を生やそうとググると、まず初めに目にするのが nuxt-community によってメンテされている [@nuxtjs/feed](https://github.com/nuxt-community/feed-module) だと思います。

良さそうに見えるんですが、静的生成するときには向かないです。

理由は2つあって、1つは [axios](https://github.com/axios/axios) で本番サーバーからデータを取ってきて RSS を生成するみたいな使い方が想定されていること、2つは RSS の生成が `generate:before` というタイミングで実行されることです。

### axios で本番サーバーからデータを取ってくることを想定されていることの何がいけないのか？

[@nuxtjs/feed](https://github.com/nuxt-community/feed-module) の example を見てみると、以下のように書かれています。

```example.js
//Import axios into your nuxt.config.js
const axios = require('axios')

// In your `feed` array:
async create (feed) {
  feed.options = {
    title: 'My blog',
    link: 'https://my-url.com/feed.xml',
    description: 'This is my personal feed!',
  }

  const posts = await (axios.get('https://api.blog.lichter.io/posts')).data
  posts.forEach(post => {
    feed.addItem({
      title: post.title,
      id: post.url,
      link: post.url,
      description: post.description,
      content: post.content
    })
  })

  feed.addCategory('Nuxt.js')

  feed.addContributor({
    name: 'Alexander Lichter',
    email: 'example@lichter.io',
    link: 'https://lichter.io/'
  })
}
```

`axios.get` でブログのAPIを叩いて、それを元に RSS を生成と言った処理が書かれていますが、これができるのはサーバーで動的にコンテンツを生成できる環境だけなので、静的生成でブログを作る際は利用できないです。

静的生成の場合、サーバーに上がっているのは、新しい記事を書く前にデプロイされたデータですからね...。

とは言え、**別に [axios](https://github.com/axios/axios) で API を叩くところを fs.readFile とかで、記事一覧の JSON ファイル取ってくれば良くね？** となるんですが...

次項の問題があります。

### RSS の生成が `generate:before` というタイミングで実行される

上記のとおりなんですが、 [@nuxtjs/feed](https://github.com/nuxt-community/feed-module) は `generate:before` というタイミングに hook して動作するように書かれています。
`generate:before` がどのようなタイミングかと言うと、まあ読んで字の如くなのですが、 generate が完了する前なので、最新の状態ではない記事一覧を参照して RSS を生成してしまいます。

しかもこれは固定なので、外から引数を渡したりして変更することができません。

generate が完了したあとに RSS 生成できるようになれば完璧なのに...！

## どうやって解決したのか

どうしようもないので、自前でモジュールを作りました。

`generate:done` に hook させて RSS を生成するようにしました。 RSS を生成する部分は [@nuxtjs/feed](https://github.com/nuxt-community/feed-module) の中を参考にして [feed](https://github.com/jpmonette/feed) を使用しました。

以下、様子です。（フィード生成の部分、ちょっと端折ってます）

```./modules/feed.js
const fs = require('fs')
const { Feed } = require('feed')
const { promisify } = require('util')

module.exports = function() {
  // generate が終わったタイミングで実行する
  this.nuxt.hook('generate:done', async generator => {
    // 全ての記事データが1つになった JSON ファイルを読み込む
    const entriesJson = await promisify(fs.readFile)('記事一覧JSONのパス', 'utf-8')
    const entries = await JSON.parse(entriesJson).data

    // 全ての記事を公開日が新しい順にソートする
    entries.sort(function(a, b) {
      return a.published_at < b.published_at ? 1 : -1
    })

    // ブログ自体のデータを流し込む
    const feed = new Feed({
      title: 'ブログ自体のタイトルを書くよ',
      description:
        'ブログ自体の説明を書くよ',
      generator: 'feed',
    })

    // 最新10件の記事データを流し込む
    for (let i = 0; i < 10; i++) {
      let entry = entries[i]

      // ブログ記事のデータを流し込む
      feed.addItem({
        title: entry.title),
        id: `https://design.dena.com/${entry.category}/${entry.id}`,
        link: `https://design.dena.com/${entry.category}/${entry.id}`,
        author: [
          {
            name: entry.author
          }
        ],
        date: new Date(entry.published_at)
      })
    }

    // RSS 2.0 形式で ./dist/feed.xml に書き込む
    await promisify(fs.writeFile)('./dist/feed.xml', feed.rss2())

    // ログ
    console.log('Output feed.xml')
  })
}
```

## まとめ

- Nuxtの静的サイトでRSSを生やしたいなら、[@nuxtjs/feed](https://github.com/nuxt-community/feed-module) を参考にしつつ自前でモジュール実装するのが良さそう
- [https://design.dena.com/feed.xml](https://design.dena.com/feed.xml) が爆誕しました