VuePressにRSSフィードを追加する

VuePressは便利なんですが製品ドキュメント向けに作られている感が強く、ブログとして使おうとすると、まだ貧弱な部分も目立ちます。今回書くRSS機能なども現在のところ用意されてはいません。(Issueで議論などはされているようなので、今後公式に実装される可能性は高いと思います。)

ここでは、公式実装が待ちきれずに作ってしまった、RSS機能を見ていきます。

要件

RSSフィード出力のために必要な機能は以下です。

  • RSS仕様に従いXML出力
  • 出力されたファイルを保存
  • 出力するファイルパスの解決

詳細については、このサイトを生成しているリポジトリのpackage.jsonを確認するのが早いですが、簡単に必要なものを挙げておくと以下のようなものになります。

  • vuepress: ^0.8.4
  • rss: ^1.2.2

なお、ファイル保存に必要なfs-extra、パス解決に必要なpathは、いずれもvuepressが依存ライブラリとして利用しているため、依存定義の必要はありません。

実装概要

VuePressでは、通常のコンテンツはHTMLとして生成されてしまうため、テーマのカスタマイズ等では今回の要件を達成することができません。そこで今回は、VuePressに用意されているApp Level Enhancementsを利用します。

App Level Enhancementsは、.vuepress/enhanceApp.jsを作成することで利用でき、この拡張ポイントでは、以下4つのパラメーターを受け取ることができます。

  • Vue
  • options
  • router
  • siteData

このうち、siteDataには、サイト内の全てのコンテンツのパース後の内容が保持されているため、これを利用することで、RSSフィードに必要な情報の一式を取り出すことができます。

こちらも詳細については、このサイトで使用しているenhanceApp.jsを確認するのが早いですが、簡単にどんなことをしているのかを紹介してみます。

export default ({
  Vue, 
  options,
  router,
  siteData
}) => {
  try {
    // `vuepress build` では参照可能、
    // `vuepress dev` では参照エラーを起こすので、
    // これを利用してDEVモード時は後続処理をスキップする
    global
  } catch (e) {
    // DEVモード時は後続処理をスキップ
    return
  }

  const path = require('path')
  const fs = require('fs-extra')
  const RSS = require('rss')

  // RSSフィードの基本情報を設定
  const feed = new RSS({
    title: siteData.title,
    description: siteData.description,
    feed_url: 'https://www.yo1000.com/rss.xml',
    site_url: 'https://www.yo1000.com/',
    copyright: '2018-present yo1000',
    language: 'ja',
  })

  // 日付の設定されていない投稿に設定するための日付初期値
  const d = new Date()
  d.setTime(0)
  // 型チェック用の日付型
  const dateType = typeof d
  
  siteData.pages.filter(page => {
    // このサイトではブログポストを `/posts` 配下に集約しているため、
    // サイト内の全てのページの中から `/posts/` で始まる、
    // なんらかの名前を持つページのみ抽出
    return /^\/posts\/.+/.test(page.path)
  }).map(page => {
    // ページメタデータの日付が設定されていない場合は、
    // 初期値を設定する
    if (!page.frontmatter.date) {
      page.frontmatter['date'] = d
      return page
    }
    // ページメタデータの日付が設定されていても日付型でない場合は、
    // 日付オブジェクトにして再設定
    if (typeof page.frontmatter.date !== dateType) {
      page.frontmatter.date = new Date(page.frontmatter.date)
    }
    return page
  }).sort((a, b) => {
    // 投稿日の降順にソート
    const aTime = a.frontmatter.date.getTime()
    const bTime = b.frontmatter.date.getTime()
    if (aTime < bTime) return 1
    if (aTime > bTime) return -1

    const aTitle = a.frontmatter.title
    const bTitle = b.frontmatter.title
    if (aTitle && !bTitle) return -1
    if (!aTitle && bTitle) return 1
    if (aTitle < bTitle) return -1
    if (aTitle > bTitle) return 1

    const aPath = a.path
    const bPath = b.path
    if (aPath < bPath) return -1
    if (aPath > bPath) return 1
    return 0
  }).map(page => {
    // ページオブジェクトを、
    // RSSフィードのアイテム形式へ変換
    return {
      title: page.title,
      description: page.frontmatter.description,
      url: `http://www.yo1000.com${page.path}`,
      date: page.frontmatter.date,
    }
  }).forEach(page => {
    // RSSフィードにアイテムを設定
    feed.item(page)
  })

  // VuePressのビルド出力ディレクトリへ
  // XML形式でRSSフィードを書き出し
  fs.writeFileSync(
    path.resolve('./docs/.vuepress/dist', 'rss.xml'),
    feed.xml()
  )
}

と、こんな具合でRSSフィードの作成ができました。

avatar

Written by yo1000 | YO!CHI KIKUCHI
Loves 🌱 Spring, 🦢 Pelikan fountain pen and 🦁 FINAL FANTASY VIII