【Nuxt3】モジュールが使えないので手動でAMP化するぞ

シェアする:

この記事をおすすめしたい人

  • Nuxt v3でAMPにしようとしてる人
  • Nuxt v3でAMPにしようとしてる酔狂な人
  • つまりオレ

Nuxt v3の調査、AMP編です。

公式を見るとですね、何やらv3でもAMPモジュール使えますよって感じに見えるんですが、実際にインストールするとこんな↓エラーが出ます。


Cannot destructure property 'nuxt' of 'this' as it is undefined.

v3でthisの扱いがけっこう変わってたのでその辺でエラー出てるのかなーと思ってます。 モジュールのリリースノート見ると2021年1月で更新止まってるし、v3には対応できてないんじゃないかなーと。

そこでv3でもどうにか手動でAMP化できないかと思ってトライする記事になります。

結論として現状は行き詰まっているので、フルAMPでしたっけ? 別ページとしてAMP版を生成するんじゃなくて、まずはサイト全体がAMPでの構築を目指します。

方針としては、

  • htmlにampを足す
  • AmpOptimeserに投げてある程度AMPにしてもらう

これで残ったエラーを潰していきます。

htmlタグにAMPを追加

とりあえずね、AMPですよ~って宣言するためにhtmlタグに「amp」を足してみましょう。

nuxt.configにこの↓ような記述を足します。


export default defineNuxtConfig({
  app: {
    head: {
      htmlAttrs: {
        amp: 'true',
      },
    },
  },
}

v3ではTypeScriptがデフォになっているのでexport defaultに型がついていること、v3の仕様でheadをappで囲っていること以外はv2と同じです。

この時点ではけっこうなエラーが出ますがまだ放置。

Amp Optimizerに丸投げ

Amp Optimizerというのは、AMPコンポーネントの必須スクリプトを自動で入れてくれたり、SSRしてくれたりとまぁテキトーに突っ込むだけでも便利な子でして。 今回はAMPモジュールくんがいないのでなるべくコイツに頼って楽しようというわけです。

コイツに処理させるにはNuxt側がHTMLの整形が終わったあと(フック)に食わせる必要があります。 v2では、nuxt.config.jsのhooksを使ってrender:routeとかgenerate:pageにAmp Optimizerの処理を挟んでいました。

ところがNuxt v3ではこれらのフックがないようです。

v3のhooks一覧からいろいろと試していて。 その時の実験結果はこの↓記事を。

フックに必要な条件は、レンダリング後のHTML全体を参照できること、加工後にそれを返せること、です。 最終的にできたのは「render:response」でした。

「render:html」もいけそうでしたが、引数に入っているのが組み立て前のHTMLのパーツのオブジェクトなので直接Amp Optimizerに食わせることはできず。 逆にバラバラの状態なので操作しやすそうな部分もありました。 この辺は上の記事を見てください。

で、この「render:response」ってのがちょっとやっかいで、NuxtのフックじゃなくてNitroとやらのフックで、nuxt.config.jsに書いても動作せず。 結論としては、プロジェクトルートの「server」というフォルダの中に「plugins」というフォルダを作り、その中にファイルを置いてこの↓ように書くようです。


export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('render:response', async (response, { event }) => {

  })
})

あとはAmp Optimizerをインストールしてこんな↓感じでOK。


import AmpOptimizer from '@ampproject/toolbox-optimizer'

const ampOptimizer = AmpOptimizer.create()

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('render:response', async (response, { event }) => {
    response.body = await ampOptimizer.transformHtml(response.body)
  })
})

出力されたHTMLのheadを確認してampprojectのスクリプトが入っていれば動作OKです。


<script data-auto="" async="" src="https://cdn.ampproject.org/v0.mjs" type="module" crossorigin="anonymous"></script>

headのゴミを削除

ここからはエラーが出ている箇所を潰して、ValidなAMPに持っていく作業(旧AMPモジュールがやっていた作業)を手動でやります。

確認してみるとheadにNuxtのscriptが入っていたのでこいつを削除します。


<link rel="modulepreload" as="script" crossorigin="" href="/nuxt/dist/app/entry.js">

AMPでは独自スクリプトは認められていない(AMPスクリプトみたいなのがあるんでしたっけ、触ったことない)ので、cheerioで全削除します。 場所は同じくrender:responseフック。


import cheerio from 'cheerio'
import AmpOptimizer from '@ampproject/toolbox-optimizer'

const ampOptimizer = AmpOptimizer.create()

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('render:response', async (response, { event }) => {
    const $ = cheerio.load(response.body, { decodeEntities: false })
    
    $('link[as="script"]').remove()
    
    response.body = await ampOptimizer.transformHtml($.html())
  })
})

cheerioを読み込んで、HTMLをcheerioにロードして、headのscriptを削除して、HTMLに戻してからAmpOptimizerに食わせています。

他のゴミがあった場合もこの段階で処理します。

bodyのゴミを削除

body末尾にscriptタグが3つ追加されていました。


<script type="application/json" id="__NUXT_LOGS__">[]</script>
<script type="application/json" id="__NUXT_DATA__" data-ssr="true">[]</script>
<script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script>

こいつらも同じようにcheerioで削除してもいいんですが、せっかくなので別のフックを使ってみます。

「render:html」フックで見た時に、この追加要素は第一引数htmlのbodyAppendに配列で入っています。 なのでこいつを空にしてやります。


export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('render:html', (html, { event }) => {
    html.bodyAppend = []
  })
})

これで無事に削除されました。

「render:html」はNitro側に載っていないのでもしかしたら削除されてる可能性がありますが、次の処理内容的にここは失敗していても大丈夫なので問題ないものとします。

ここまででHTMLが空だった場合はValidなAMPになっていると思います。

amp-imgなどの専用タグでエラーが出る

AMPではamp-imgなど、「amp-」から始まる独自タグが追加されます。 一覧はAMPモジュールのコレかなたぶん。

初期状態ではNuxtではこれらは「そういう名前の(Nuxtの)コンポーネント」として理解するみたいで、このままではエラーが出ます。


[Vue warn]: Failed to resolve component: amp-img

エラーメッセージで検索するとVueの方のページが出て来ました。 これとNuxtのドキュメントを合わせていろいろ試していると、こんな↓格好で動きました。


export default defineNuxtConfig({
  vue: {
    compilerOptions: {
      isCustomElement: (tag) => {
        return tag.startsWith('amp-')
      },
    },
  },
}

これで「amp-」で始まるタグのエラーが無視できるので、AMPコンポーネントでエラーが出なくなります。

body直下にないといけないコンポーネント対策

AMPコンポーネントにはbody直下にないとエラーが出るものがあります。 覚えてるのはamp-sidebarがそうだったと思いますが、なんか他にもあったかも。

Nuxtはデフォだとbody直下にdivの多段攻撃になるので、AMPコンポーネントがちゃんと動作してくれません。 旧AMPモジュールでもその対策がされていて、どうもamp-bodyというタグをbody直下に再配置しているような感じでした。

なのでAMPモジュールをまねて、body直下に配置する要素をamp-bodyタグとして、やってみようと思います。

pagesのindex.vueとか、レイアウトとか、v3だとapp.vueとか、どこでもいいですがルートになる要素にamp-bodyタグをつけます。 そして上のrender:responseフックにこの↓ような処理を追加します。


$('body').html($('amp-body').html())

amp-bodyタグの中身をbodyの中身と入れ替えています。

場所はAmpOptimeserに食わせる前ならどこでも大丈夫そう。

CSSの処理

難航してます。

要件としては次の2つ。

  • nuxt.configやコンポーネントで定義したCSSの全てを回収する
  • <style amp-custom>として全てのCSSをインラインで埋め込む

v2用のAMPモジュールを見ると「vue-renderer:ssr:templateParams」というフックの中でこの処理をしていました。

同じようにフックで処理しようとv3のフックを全て調べたんですが、最終段階のafterResponseでもCSSが展開されてなくて(Viteの処理前)、今の知識ではちょっとどうにもなりませんでした。

いくつか思いつく方法を試してるんですがうまくいってません。

逆にここをクリアできる書き方、例えば計算が不要な生のCSSを自分で<style amp-custom>に入れるとか、それが可能ならAMP化ができると思います。

amp-mustacheがなんかトラブる?

AMPモジュールをざーっと見てみたらamp-mustacheだけなんか特殊な処理をされてました(詳しく見てない)。

amp-mustacheを使って動いてるプロジェクトもあるので、それを移植した時に調べておきます。

まとめ

いやね、これ、完結してないんですよ。

頑張った分は追記していくので許してください。

海外フォーラムでね、「v3だとAMPは使えないのかよ…」って悲しんでたアニキがいたので、俺は彼を救うんだ…。

シェアする: