Nuxt.js の Sitemap Module を SSR 環境下で利用する

Nuxt.js の Sitemap Module について、何故か Generate による Static Page / Full Static でしか利用できないと思いこんでいたのでメモ。

Sitemap Module について

Nuxt.js オフィシャルに提供されているサイトマップの生成用モジュールであり、基本的にはモジュールとして読み込むだけで自動でサイトマップを生成してくれる。

基本的には $ yarn add @nuxtjs/sitemap を行い、 nuxt.config.js にて modules 指定を行うだけ。

https://www.npmjs.com/package/@nuxtjs/sitemap

サイトマップを必要とする多くのサイトで利用されており、大体 weekly download が 30k 程度と人気のモジュールとなっている。

対象となるエンドポイントのカスタマイズ

Sitemap Module には、オプションで nuxt.config.js を拡張する形で sitemap: object[] | object | () => object | false | false を受け取るような仕組みが用意されており、これを利用することで、サイトマップ生成に関する様々な拡張を行うことができる。

そんな Sitemap の項目の一つに routes という項目があり、これは string | object の配列を返す関数を定義することで、手動でサイトマップのリストを定義できる。

import { NuxtConfiguration } from '@nuxt/types'

const config: NuxtConfiguration = {
  sitemap: {
    hostname: 'https://example.com',
    routes() {
      return [
        '/',
        '/about',
        '/contact',
      ]
    }
  }
}

export default config

こんな感じ。

基本的にはこれが本番を想定したビルドタイムでのみ処理されると考えていたが、あるタイミングから generate: Boolean が追加されたり、今だと平然と generate 以外のシチュエーションが想定されている。

Sitemap Module の動き

Nuxt.js のファイル生成系のモジュールは大きく以下の2つにわけられる。

  1. nuxt buildnuxt generate 時に解決される静的出力
  2. serverMiddleware を利用した動的出力

Sitemap Module は、 generate 時には 1 を、 SSR 時には 2 の戦略を自動的にスイッチしてくれる。

具体的には以下のようなコードが記述されている。

// https://github.com/nuxt-community/sitemap-module/blob/dev/lib/middleware.js
  // Add server middleware for sitemapindex.xml
  nuxtInstance.addServerMiddleware({
    path: options.path,
    handler(req, res, next) {
      // Init sitemap index
      const xml = createSitemapIndex(options, base, req)
      // Check cache headers
      if (validHttpCache(xml, options.etag, req, res)) {
        return
      }
      // Send http response
      res.setHeader('Content-Type', 'application/xml')
      res.end(xml)
    },
  })

該当のソースコード

routes の動的出力

というわけで本題の routes を動的に生成について。これは基本的に Full Static 時に CMS などにアクセスする場合の routing 設定と同じようにすると OK。

例えば WordPress REST API にアクセスするシチュエーションを想定する。

ページネーションやエラーハンドリングなどは省略するが、一例として pages/_id.vue には以下のような記述がある場合、

<template>
  <section>
    <h1></h1>
    <div v-html="post.content.rendered">
    </div>
  </section>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  async asyncData({ app, params }) {
    const [ post ] = await app.$axios.get(`/posts?slug=${params.slug}`)
    return {
      post
    }
  }
})
</script>

サイトマップの routes の定義は以下のようになる。

import axios from 'axios'
import { NuxtConfiguration } from '@nuxt/types'

const API_ROOT = 'https://example.com/wp-json/wp/v2'

const config: NuxtConfiguration = {
  sitemap: {
    hostname: 'https://example.com',
    async routes() {
      const apiClient = axios.create({
        baseUrl: API_ROOT
      })
      const { data } = await apiClient.get('/posts/')
      return data.map((post) => `/posts/${post.slug}`)
    }
  }
}

export default config

基本的には SSR 時の generate と同じであるが、 @nuxtjs/pwa などとは違い、SSR モードの場合はビルドタイムではなくランタイムでこの中身が適切に処理される。

cacheTime のオプションも存在するため、生成にあたってある程度の負荷が予想されるエンドポイントであっても、キャッシュ設定を行っておけば問題ない。