Hiratake Web

Nuxt Content で RSS フィードを配信する

投稿した日
更新した日
書いたひと
Hiratake

NuxtNuxt Content モジュールを使用しているウェブサイトで、RSS フィードを配信したときの対応内容を、備忘録的に残しておこうと思います。

環境

今回作業した環境は以下のとおりです。

/blog ディレクトリの下にブログ記事が存在し、そのブログ記事の更新情報を RSS フィードで配信します。

web/
 ├ content/
 │ ├ blog/
 │ │ ├ article01.md
 │ │ ├ article02.md
 │ │ └ index.md
 │ └ index.md
 ├ server/
 │ └ tsconfig.json
 ├ app.vue
 ├ nuxt.config.ts
 ├ package.json
 ├ pnpm-lock.yaml
 └ tsconfig.json

サーバルートを追加する

Nuxt Content のドキュメントにはサイトマップを生成する方法が掲載されています。こちらを参考に、RSS フィード用のルートを作成します。

server/ ディレクトリ内に routes/ ディレクトリを作成し、その中に feed.xml.ts というファイルを作成します。 server/routes/ にファイルを配置すると、ウェブサイト公開時に /feed.xml という URL でアクセスが可能になります。

作成した feed.xml.ts に、Nuxt の サーバディレクトリについてのドキュメント にある例のコードを追加して、表示を確認します。

export default defineEventHandler((event) => {
  return {
    hello: 'world'
  }
})

pnpm dev で開発サーバを起動し、http://localhost:3000/feed.xml へアクセスすると、return で返している情報が表示されることが確認できます。

サーバディレクトリに追加したファイルの確認の画像

RSS フィードを生成する

実際に配信する RSS フィードの内容を生成します。この記事では、npm で「rss generator」などで調べたときに一番上に出てくる feed パッケージを利用します。

先ほど作成した feed.xml.tsfeed パッケージをインポートし、defineEventHandler の引数として与えている関数内に RSS フィードを生成する処理を追加します。 feed パッケージの使用例を参考に、以下のようなコードを追加します。

import { Feed } from 'feed'

export default defineEventHandler((event) => {
  const feed = new Feed({
    title: 'ウェブサイトのタイトル',
    description: 'ウェブサイトの説明',
    id: 'https://example.com/',
    link: 'https://example.com/blog/',
    language: 'ja',
    image: 'https://example.com/image.png',
    copyright: 'コピーライト',
  })

  // RSS 2.0 形式で出力する
  return feed.rss2()
})

表示を確認すると、Content-Type が text/html となっています。nuxt.config.ts を編集し、application/rss+xml に変更します。

export default defineNuxtConfig({
  modules: ['@nuxt/content'],
  routeRules: {
    '/feed.xml': {
      headers: { 'content-type': 'application/rss+xml; charset=UTF-8' },
    }
  },
  ...
})

続いて、ブログ記事のデータを追加していきます。サーバディレクトリ内で記事のデータを取得するには serverQueryContent 関数を使用します。今回作業する環境では、ブログ記事は全て Markdown 形式で作成しているため、型引数に MarkdownParsedContent を指定します。

また、ブログ記事には全て created という名前で、投稿日の情報を YAML Front Matter に含めています。

import type { MarkdownParsedContent } from '@nuxt/content/dist/runtime/types'
import { Feed } from 'feed'

export default defineEventHandler((event) => {
  ...
  const articles = await serverQueryContent<MarkdownParsedContent>(event, 'blog')
    .limit(10)
    .find()

  articles
    // 拡張子が「.md」かつ内容が空ではないものを絞り込み
    .filter((article) => article?._extension === 'md' && !article._empty)
    .forEach((article) => {
      const url = `https://example.com${article._path}/`
      // ブログ記事を追加
      feed.addItem({
        title: article.title,
        id: url,
        link: url,
        description: article.description,
        date: new Date(Date.parse(article.created)), // YAML Front Matter の created
      })
    })
  ...
}

これで表示を確認すると、ブログ記事の情報が RSS フィードに追加されていることが確認できます。

記事本文の内容を含める

RSS フィードには、ブログ記事の本文を含めることができます。serverQueryContent から取得したデータには本文の情報も含まれていますが、MarkdownNode という型のオブジェクトの配列となっており、そのままでは使用できません。

そのため、本文の内容を生成するための関数を実装する必要があります。方法は色々あるのではないかと思いますが、 今回は力技でどうにかします 。他に良き方法があれば教えてください…。

/server/utils/ ディレクトリに content.ts ファイルを作成し、generateContentFromAst という関数を作成します。

import type { MarkdownParsedContent } from '@nuxt/content/dist/runtime/types'

export const generateContentFromAst = (
  children: MarkdownParsedContent['body']['children']
): string => {
  let text = ''

  for (const node of children) {
    let startTag = ''
    let endTag = ''

    if (node.type === 'element' && node?.tag) {
      if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'ul', 'ol', 'li', 'blockquate', 'em', 'strong', 'del', 'pre', 'code'].includes(node.tag)) {
        // <p>テキスト</p> のようにテキストの前後にタグがついてるもの
        startTag = `<${node.tag}>`
        endTag = `</${node.tag}>`
      } else if (['hr', 'br'].includes(node.tag)) {
        // <hr /> のようにタグ単体で完結するもの
        text += `<${node.tag} />`
      } else if (['a'].includes(node.tag)) {
        // リンク
        const href = (node?.props?.href as string) || ''
        startTag = `<${node.tag} href="${href}">`
        endTag = `</${node.tag}>`
      } else if (['img'].includes(node.tag)) {
        // 画像
        const src = (node?.props?.src as string) || ''
        const alt = (node?.props?.alt as string) || ''
        text += `<${node.tag} src="${src}" alt="${alt}" />`
      }
    }

    if (node.type === 'text') {
      text += `${(node?.value || '').trim()}`
    }

    if (node?.children) {
      text += `${startTag}${generateContentFromAst(node.children).trim()}${endTag}`
    }
  }

  return text
}

最後に、先ほど作成した generateContentFromAst を使用して、RSS フィードに本文の内容を追加します。

/server/utils/ 内のファイルでエクスポートした関数は自動的にインポートされるため、そのまま使用することができます。

export default defineEventHandler((event) => {
  ...
      feed.addItem({
        title: article.title,
        id: url,
        link: url,
        description: article.description,
        content: generateContentFromAst(article.body.children), // 本文の内容
        date: new Date(Date.parse(article.created)),
      })
  ...
}

これで表示を確認すると、記事本文の内容が追加されていることが確認できます。

SSG の場合の設定を追加する

nuxt generate で SSG を行っている場合は、nuxt.config.ts に以下のような設定を追加して /feed.xml が生成されるようにする必要があります。

export default defineNuxtConfig({
  modules: ['@nuxt/content'],
  nitro: {
    prerender: {
      routes: ['/feed.xml'],
    },
  },
  routeRules: {
    '/feed.xml': {
      headers: { 'content-type': 'application/rss+xml; charset=UTF-8' },
    }
  },
  ...
})

以上で完了です。おつかれさまでした。

参考