Nuxt.js の Content Module(nuxt/content) を利用している Web サイトを AMP に対応させる

@​nuxtjs/amp に投げていたパッチが無事マージ・リリースされたので、 @​nuxt/content を利用した静的 Web サイトを AMP 対応させる方法について紹介したいと思います。

なお、本ブログは現在、通常のページと AMP に対応したページを併せて提供しています。

Nuxt.js における AMP 対応について

Nuxt.js といえば豊富なエコシステムやモジュールによる機能追加が魅力の一つとしてあげられますが、その例にもれず、AMP対応もオフィシャルのモジュールにて行うことができます。

基本的には @​nuxtjs/amp を利用し、依存関係に追加したあと、以下を nuxt.config.js に記述するだけで、ひとまずの AMP 提供が可能となります。

import { NuxtConfigration } from '@​nuxt/types'

const config: NuxtConfiguration = {
  modules: [
    '@​nuxtjs/amp'
  ]
}

export default config

もちろん、正しく対応するためには(例えば $isAMP が true の場合は <img> ではなく <amp-img> を利用するなど)ユーザー側で定義したコンポーネントを AMP 対応させる必要がありますが、これのみで基本的な HTML meta の提供、すべてのエンドポイントに対して、 /amp/${route} の形での URL の発行などを自動的に行います。

ユーザー側のコードは自身である程度取り扱うことができるものの、 @​nuxt/content と噛み合わせる場合にいくつか工夫を行う必要があったため、かんたんに紹介します。

Content Module と AMP Module の競合について

Content Module は JSON や Markdown から HTML とルーティングを生成するモジュールであり、 AMP Module は Nuxt の出力結果のルーティング一覧と HTML を直接書き換えるモジュールとなります。

そのため、 Content Module にいくつかの変更を加えない場合、 AMP Module の機能が競合するケースがあります。

以下に主な問題と対処法を掲載しておきます。

ルーティング漏れ

nuxt generate でルーティングを生成する場合、基本的に SSG 環境では以下のようにルーティングが定まります。

  1. pages/ 配下に存在する静的なルーティングを Nuxt.js 側で検知
  2. そこから辿ることができるすべてのルーティングを検知
  3. routes オブジェクトや配列、または routes 関数の結果を受け取り、検知した内容と concat
  4. hook をコールし、モジュールが動作

この 4. の部分が曲者で、 Content Module と AMP Module が同時並行で動いてしまうため、結果として Content Module にて追加されるルーティングを AMP Module が検知できない状況となります。

対処方法はシンプルに、普段 Content Module ではなく CMS などにアクセスしている場合と同じように、明示的に routes にて指定してやることです。

このブログでは、以下のように定義しています。

const config = {
  generate: {
    async routes () {
      const { $content } = require('@​nuxt/content')
      const files = await $content('entry').fetch()

      return [
        '/',
        ...files.map(file => file.path),
        ...files.map(file => '/amp'+file.path),
      ]
    }
  }
}

<img> などの HTML タグの対応

Content Module はデフォルトでは <nuxt-content /> コンポーネントに、モジュールが解析した HTML AST を渡してレンダリングします。

そのため基本的にこの部分には介入できないのですが、介入しない場合 AMP ページで <img> タグのまま表示されることとなるため、これらは対処する必要があります。

幸い AMP Module は $isAMP を inject してくれているため、これを利用することで出し分けを行うことができます。

AMP のカスタムタグは大量にありますが、基本的にこのブログではほとんどが通常のテキスト + 画像のみで完結しているため、以下のようなコードを書いて対応することとしました。

厳密性を求める場合は、AMP の公式のタグの一覧の配列を定義してやると良いかと思います。

export default Vue.extend({
  async asyncData({ app, $content, params }) {
    const entry = await $content(`entry/${params.slug}`).fetch<Entry>();

    if (app.$isAMP) {
      type Node = { children?: Node[], props: object, tag: string, type: string }
      function toAMPImage(node: Node) {
        if (node.tag === 'img') {
          node.tag = 'amp-img'
          node.children = []
        }
        if (node.children) {
          node.children = node.children.map((child): Node => {
            return toAMPImage(child)
          })
        }
        return node
      }

      entry.body = toAMPImage(entry.body)
    }

    return {
      entry,
    };
  },
})

AMP 対応のチェック

以上で AMP 対応は完了となるため、ngrok なりで表示を確認します。

有効な AMP ページとなっていれば実装完了です。デプロイしてしまいましょう。

余談

@​nuxt/content@​nuxtjs/amp を組み合わせた情報が世の中になく、試しにこのブログを対応させてみた中でわかったこととなります。

基本的には最低限の対応で済んだのですが、AMP module 側に正規表現の誤りがあり、複数の CSS がある場合に全てを消し飛ばしてしまう現象に悩まされました。

([\S\s]* を指定してバグが起こるのは正規表現サボったときにありがちですね)

実は年末年始の間にこのブログ自体の AMP 対応は完了していたのですが、送ったパッチがマージされない限り、実用できる Web サイトが限られそうだったので、ひとまず誰でも AMP 対応を試せるような状態になってからの公開としました。

現状最新版の v0.5.3 を利用すると問題なく AMP 対応できるようになっているはずなので、ぜひ。