GatsbyJS製の本ブログをAMP対応しました
本ブログ(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-amp
やgatsby-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.js
はgatsby-amp-starter-blog
から拝借し、自分のサイトに合うように修正します。
ampifyjsなどのモジュールをインストールする
まず、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にampify
とamp: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-images
とgatsby-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ファイルを取得します。
{
"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/
を検査してみましょう。
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 🎉