GatsbyJS製の本ブログをAMP対応しました

@Panda_Program

本ブログ(Gatsby製)をAMP対応しました

GatsbyをAMP化するPluginはまだ存在しない

AMPとはウェブサイトを高速で表示するためのHTML/CSS/JavaScriptの書き方です。AMPは「Accelerated Mobile Pages」の略で、Googleはニュースサイトに対して何ができるかという問いから始まったプロジェクトです。

**その来歴から、AMPのユースケースはブログのような静的サイトにピッタリです。**しかも、AMP対応のページはGoogleがキャッシュしてくれたり、検索結果で上位に表示されたり、chromeを立ち上げた時のオススメwebページの紹介の中に掲載される可能性があったりといいこと尽くしです。

一方、**GatsbyJS(以下Gatsby)はReactでコンポーネントを記述し、SSRでHTMLを生成します。**また、Plugiプラグインを導入してカスタマイズを柔軟に行えるという特徴がああります。そうであれば、AMP化するPlugiプラグインを使って、SSR時にAMP用のHTMLを吐き出せば誰でもAMP対応のGatsby製のサイトが作れそうに思います。

実際、GitHubのissueが2019年4月に立ち40件以上のコメントが付いていおり、gatsby-plugin-ampgatsby-plugin-html2ampといったプラグインに言及されています。しかし、どちらのPluginもうまくいかないケースがあるようで、GatsbyをAMP化する完全なプラグインは未だに実現されていません。

gatsby-amp-starter-blogというレポジトリを参考に実装する

それでもAMP化を諦めずに調べていると、gatsby-amp-starter-blogというレポジトリに行き当たりました。

このレポジトリの方法でGatsby v2.23.3の当ブログでもAMP対応ができましたので手順をご紹介します。

GatsbyをAMP対応する手順を紹介します

gatsby-amp-starter-blogは最終更新が3年前であり、そのまま使うことはできないため、AMP化に必要なところだけをPickupしていきます。

**GatsbyのAMP化とは、つまりGatsbyで吐き出したHTMLをAMPに対応するように書き換えるというものです。**具体的には、以下の手順になります。

  • $ gatsby buildでHTMLを生成する
  • ampify.jsを作成し、$ node ampify.jsでHTMLをAMPように書き換える

ampify.jsgatsby-amp-starter-blogから拝借し、自分のサイトに合うように修正します。

ampifyjsなどのモジュールをインストールする

まず、ampify.jsをルートディレクトリにコピーします。

ざっくり処理の流れを追えるように省略したりコメントを書きました。

ampify.js
const recursive = require('recursive-readdir');
const fs = require('fs');
const ampify = require('ampifyjs');
const sass = require('node-sass');

const GA_TRACKING_ID = 'UA-SOME_ANALYTICS_ID-1';
const inputDir = 'public/amp';
const filesToConvert = [];

recursive(inputDir, [], (err, files) => {
  // public/ampにある拡張子がhtmlであるファイル名を取得
  for (const file of files) {
    if (file.endsWith('.html')) {
      filesToConvert.push(file);
    }
  }

  for (const fileToConvert of filesToConvert) {
    const urlPath = fileToConvert.replace(inputDir, '');
    const contents = fs.readFileSync(fileToConvert, 'utf8');
    // htmlファイルを読み込んで、ampifyjsでAMP化する
    fs.writeFileSync(fileToConvert, ampify(contents, urlPath, ($) => {
      // 自分のHTMLを書き換える
    }), 'utf8');
  }
  console.log('The site is now AMP ready');
});

次に、ampify.jsを動かせるようにしましょう。まずはnpmモジュールをインストールします。

$ npm install -S recursive-readdir ampifyjs node-sass

package.jsonにampifyamp:buildコマンドを追加します。

{
  "scripts": {
    // ...
    "ampify": "cp -r public amp && rm amp/*.js amp/*.map amp/*.css amp/*.xml amp/*.json amp/*.txt amp/*.webmanifest && mv amp public/amp && node ampify.js",
    "amp:build": "gatsby build && npm run amp:prepare && node ampify.js",
  }
}

そして、amp:buildを実行してみましょう。amp対応ファイルがpublic/amp以下に吐き出されます。$ gatsby serveでローカルサーバーを立ち上げ、http://localhost:9000/amp/posts/[slug](slugはブログ記事のslug)にアクセスします。

開発者ツールでhtmlを確認すると、imgタグはamp-imgなどamp対応のタグに書き換わっていることがわかります。

ampify.jsを書き換える

まずは、GoogleのAMP対応確認ツールにAMP化したHTMLコードを貼り付けましょう。

ツールで検出されたエラーを1つずつ潰していきます。以下では、私のサイトで検出されたエラーと解消方法を紹介します。

amp-imタグにwidthとheightを設定する

amp-imタグはwidthとheightが指定されていないとAMP対応のページと見なされません。このため、ampify.js内でamp-imgタグに画像サイズを指定していきます。

fs.writeFileSync(fileToConvert, ampify(contents, urlPath, ($) => {
  // ...
  $('amp-img').attr('layout', 'responsive');
  $('amp-img').attr('width', '450');
  $('amp-img').attr('height', '270');
  // ...
}))

また、AMPはimgタグをLazy-Loadingに対応させるための属性loadingに対応していないため、こちらを削除します。

$('amp-img').removeAttr('loading');

pictureタグをdivタグに書き換える

**pictureタグはCSSやJSを使わずHTMLだけで画面の幅に応じて画像を出し分けるタグです。**HTML5で登場しました。

Gatsbyのプラグインgatsby-remark-imagesgatsby-remark-relative-imagesは、マークダウンの![alt](/src)をimgタグを囲んだpictureタグに書き換えてくれます。

<a class="gatsby-resp-image-link" href="/static/foo.png">
  <span class="gatsby-resp-image-background-image"></span>
  <picture>
    <source srcset="/static/foo.webp 240w,
        /static/foo.webp 480w,
        /static/foo.webp 960w,
        /static/foo.webp 1244w"
        sizes="(max-width: 960px) 100vw, 960px"
        type="image/webp"
    >
    <source srcset="/static/foo.png 240w,
        /static/foo.png 480w,
        /static/foo.png 960w,
        /static/foo.png 1244w"
        sizes="(max-width: 960px) 100vw, 960px"
        type="image/png"
    >
    <img class="gatsby-resp-image-image" src="/static/foo.png"
     alt="GASの画面" title="GASの画面" loading="lazy" />
  </picture>
</a>

ビルド時にpngとwebpという拡張子で画像を作成し、それぞれ4種類のサイズを用意しています。画面幅に応じて、最適な画像サイズで配信されます。

しかし、AMPではaタグの中でpictureタグは使えません。そこで、pictureタグの中のsourceタグを削除し、imgタグだけを残すようにします。

const pictures = $('picture');

if (pictures.length > 0) {
// pictureタグ内のsourceを削除
pictures.children('source').remove();
// pictureタグをamp-imgタグに変更
pictures.each((_, element) => {
  const ampImg = $(element).html().trim();
  $(element).replaceWith(ampImg);
});
}

これで、タグを書き換えられました。

<a class="gatsby-resp-image-link" href="/static/6acb661c2b99fc28ddc98c515e3b5d5b/5b6ee/2020_06_22__0.png" target="_blank" rel="noopener">
  <span class="gatsby-resp-image-background-image"></span>
  <amp-img class="gatsby-resp-image-image i-amphtml-element i-amphtml-layout-responsive i-amphtml-layout-size-defined i-amphtml-layout"
    src="/static/foo.png"
    alt="GASの画面" title="GASの画面" layout="responsive"
    height="270" width="450" i-amphtml-layout="responsive"
  >
    <i-amphtml-sizer slot="i-amphtml-svc" style="padding-top: 60%;"></i-amphtml-sizer>
    <img decoding="async"
      alt="GASの画面" src="/static/foo.png" title="GASの画面"
      class="i-amphtml-fill-content i-amphtml-replaced-content"
    >
  </amp-img>
</a>

CSSの読み込み方法を変更する

GatsbyはCSSをmoduleとしてコンポーネント内でimportすると、CSSをJavaScriptオブジェクトとして扱います。

import React from "react"
import style from "./meta.module.css"

export default function Meta({ children }) {
  return (
    <h1 className={style.title}>{children}</section>
  )
}

JSのビルド後のクラス名は以下のように変換されます。

Meta-module--title--29eD7

これは[コンポーネント名]-[モジュール名]--[クラス名]--[hash]に対応しています。目的は、クラス名を重複させず、コンポーネントに当てるCSSのスタイルを一意にするためです。

今回のAMP対応ではhashの扱いが鬼門です。

上記のような処理をしていない場合は、単純にCSSファイルを文字列化してビルド後に生成されるHTMLに埋め込めば、AMPページでもCSSを当てることができます。

しかし、hashはビルドごとに変化します。つまり、ビルド後にhashが決められたCSSファイルを読み込んで、AMP化するHTMLに埋め込む必要があるのです。ただラッキーなことに、Gatsbyはビルド時にCSSを単一のファイルにまとめてくれています。

そこで、webpackの生成物webpack.stats.jsonからバンドルされたCSSファイルを取得します。

webpack.stats.json
{
  "namedChunkGroups": {
    "app": {
      "assets": [
        "webpack-runtime-60beaf2db0953a9c3aa4.js",
        "webpack-runtime-60beaf2db0953a9c3aa4.js.map",
        "styles.1aa66f171d481e9f3c38.css", // ビルドされたCSS
        "styles-0dd9b16d06f2e4f550cc.js",
        "styles-0dd9b16d06f2e4f550cc.js.map",
        "framework-7656862d80676d58607a.js",
        "framework-7656862d80676d58607a.js.map",
        "532a2f07-7b955a90846d24fe3005.js",
        "532a2f07-7b955a90846d24fe3005.js.map",
        "app-2898dff8cdf9028680fe.js",
        "app-2898dff8cdf9028680fe.js.map"
      ],
    },
    // ...
  }
}

今回ビルドされたCSS名はstyles.1aa66f171d481e9f3c38.cssです。このCSSを読み込み、文字列に変換します。

const webpackStats = JSON.parse(
  fs.readFileSync('public/webpack.stats.json')
);

// cssのファイル名を取得する
const files = webpackStats.namedChunkGroups.app.assets
  .filter((file) => file.endsWith('.css'));

// node-sassでCSSを文字列にする
let css = files.map((file) => sass.renderSync({
  file: `public/${file}`,
  outputStyle: 'compressed'
}).css.toString()).join('');

// SVGに当てるCSSも読み込みます
css += sass.renderSync({
  file: 'src/assets/amp/svg.scss',
  outputStyle: 'compressed',
}).css.toString();

これでAMPページに通常ページと同じCSSを適用できました。

Netlifyにデプロイする

netlify.tomlを書き換えて、ビルドコマンドを変更します。

[build]
  publish = "public"
- command = "npm run build"
+ command = "npm run amp:build"
[build.environment]
  NPM_VERSION = "6.12.0"

Search ConsoleでAMP対応を確認する

Search ConsoleのURL検査の項目から、サイトがAMPに対応しているかチェックできます。https://panda-program.com/amp/posts/clasp-typescript/を検査してみましょう。

Search Console

AMPに対応していることが確認できました。

これから対応すること

amp-imgタグはwidthとheightを指定する必要があります。私のサイトの画像は縦横比が様々なので、縦横を固定の数値で指定すると画像が歪んでしまいます。

アスペクト比で記述することもできるのですが、どちらにしろアップロードしている画像自体を全て同じサイズで作成しなければいけないなと思っています。

まとめ

GatsbyでAMP対応のページを作成できました。導入しているプラグインによっては、さらに別の対応が必要になるかもしれません。

当サイトで実行しているampify.jsはGitHubで公開していますので、AMP化の際には参考にしてみてください。正式な対応には、Gatsbyの公式からAMPに対応する方法が公開されるのを待ちましょう。

ちなみに、Next.jsはAMP Optimizerでサイトを手軽にAMP化できます。最初からAMP対応のサイトを作るなら、Next.jsの方がいい選択肢だと思っています。ご参考まで。

私はGatsbyもNext.jsもどちらも好きです 😊

(2020/7/11追記)AMPページと元のページでアクセスが分かれてしまうため、SEOを考慮してAMPページを削除しました。AMPページ1本に絞ればURLが分かれる問題は解決できますが、今回は見送ります。

参考

Happy Coding 🎉

パンダのイラスト
パンダ

記事が面白いと思ったらツイートやはてブをお願いします!皆さんの感想が執筆のモチベーションになります。最後まで読んでくれてありがとう。

  • Share on Hatena
  • Share on Twitter
  • Share on Line
  • Copy to clipboard