Next.js 製サイトに AMP を適用する

/
#nextjs#typescript#amp

introduction

Too Long, Didn’t Read

[slug].tsxgetStaticPaths などを使った dynamic route と、Hybrid AMP を併用することは現状難しいことが判明した。色々考えた結果、https://oriverk.dev の方はコードの自由度を保つため、AMP 技術を組み込まないことにした。その他の playground や趣味ブログで使っていきたい。

AMP(Accelerated Mobile Pages)

Google と Twitter による開発のキャッシュ等によるモバイル表示の高速化技術。AMP Websites, Stories, Ads, Email の 4 つがあり、検索ページでは AMP 対応サイトは雷⚡アイコンが表示される。今回は AMP Websites を利用する。

reference: amp.dev - AMP HTML 仕様

undefined
1
<!doctype html>
2
<html >
3
<head>
4
<meta charset="utf-8">
5
<title>Sample document</title>
6
<link rel="canonical" href="./regular-html-version.html">
7
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
8
<style amp-custom>h1 {color: red}</style>
9
<script type="application/ld+json">
10
{
11
"@context": "http://schema.org",
12
"@type": "NewsArticle",
13
"headline": "Article headline",
14
"image": [
15
"thumbnail1.jpg"
16
],
17
"datePublished": "2015-02-05T08:00:00+08:00"
18
}
19
</script>
20
<script async custom-element="amp-carousel" src="https://cdn.ampproject.org/v0/amp-carousel-0.1.js"></script>
21
<script async custom-element="amp-ad" src="https://cdn.ampproject.org/v0/amp-ad-0.1.js"></script>
22
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
23
<script async src="https://cdn.ampproject.org/v0.js"></script>
24
</head>
25
<body>
26
<h1>Sample document</h1>
27
<p>Some text<amp-img src=sample.jpg width=300 height=300></amp-img></p>
28
</body>
29
</html>

Next.js と AMP

Next.js は AMP に対応していて、AMP のみの生成と AMP と従来の HTML ページの生成を制御できる。export const config = { amp: true } または ‘hybrid’としておけば、amp コンポーネント用の script などの記述が自動で挿入される。

AMP First Page

  • nextjs と react client side のランタイムを持たない
  • amp optimizer で自動最適化
  • ユーザー用の最適化済みページと、検索エンジン用のインデックス可能な非最適化ページを生成
undefined
1
export const config = { amp: true }
2
const Component = () => {
3
return <h3>My AMP About Page!</h3>
4
}
5
export default Component

Hybrid AMP Page

  • 従来の HTML ページと AMP ページが生成される
  • AMP ページは amp-optimizer により最適化されているため、検索エンジンによるインデックスが可能

なお、amp-only と hybrid の 2 つのモードの区別には、useAmp() という React Hooks が用いられる。前者の時は true を、後者の時は false を返す。

undefined
1
import { useAmp } from "next/amp";
2
3
export const config = { amp: "hybrid" };
4
const Component = () => {
5
const isAmp = useAmp();
6
return (
7
<>
8
{isAmp ? (
9
<amp-img
10
layout="responsive"
11
width="300"
12
height="300"
13
src="/my-img.jpg"
14
alt="a cool image"
15
/>
16
) : (
17
<img width="300" height="300" src="/my-img.jpg" alt="a cool image" />
18
)}
19
</>
20
);
21
};
22
export default Component;

Main

修正する必要がある箇所

setup amp-validator

google web store - AMP Validator を使用する。

modify components for amp

style amp-custom

css ライブラリには styled-jsx を使ってます。非 amp の時の様に <style jsx>{ amp-img { width: 100%; } }</style> の様に書けば、自動的に <style amp-custom> に変換される。

amp.d.ts

AMPはtypescript用の組込型が無い ので、自分で amp.d.ts を作る必要がある。実際に <amp-img> とすると Property 'amp-img' does not exist on type 'JSX.IntrinsicElements'. と出る。

Image from Gyazo

custom types の追加には stack overflow を参照する様にと Next.js 公式ドキュメントにある。

取り敢えず、amp-image を対応してみる。

amp.d.ts
1
declare namespace JSX {
2
type ReactAmp = React.DetailedHTMLProps<
3
React.HTMLAttributes<HTMLElement>,
4
HTMLElement
5
>;
6
7
interface AmpImg extends ReactAmp {
8
children?: React.ReactNode;
9
alt?: string;
10
attribution?: string;
11
src?: string;
12
srcset?: string;
13
width?: string;
14
height?: string;
15
sizes?: string;
16
heights?: string;
17
layout?: "fill" | "fixed" | "fixed-height" | "flex-item" | "intrinsic" | "nodisplay" | "responsive";
18
fallback?: "";
19
20
on?: string; // amp-image-lightbox
21
role?: string;
22
tabindex?: string;
23
}
24
25
interface IntrinsicElements {
26
"amp-img": AmpImg;
27
}
28
}

amp-img

以前に画像最適化した際 に使った next-optimized-images を今回も併用した。

また fallback にはエラー回避のために、空文字を渡しておいた。これは Reactの仕様に起因 していて、React issue#9230 が一番参考になった。これによる Next.js 側の issue だと、#8861#10000#12708 がある。attribute が違うだけで、原因は全部同じようだ。

undefined
1
const AmpImg = () => {
2
// below is related to next-optimized-images
3
const image = require("@public/assets/shirase.jpg?resize");
4
const webp = require("@public/assets/shirase.jpg?resize&format=webp");
5
return (
6
<amp-img alt="shirase" layout="responsive"
7
width={webp.width} height={webp.height} src={webp.src} srcset={webp.srcSet}
8
>
9
<amp-img fallback="" alt="shirase"
10
width={image.width} height={image.height} src={image.src} srcset={image.srcSet}
11
></amp-img>
12
</amp-img>
13
);
14
};

amp-image-lightbox

画像ポップアップの lightbox。amp-image-lightbox を書き加え、amp-img に on 属性などを書き足すだけで動く。また id さえ合致して置けば、1 ページに 1 つの amp-image-lightbox で動く。

undefined
1
const AmpImageLightbox = () => {
2
const shirase = require("@public/assets/shirase.jpg?resize");
3
const pikachu = require("@public/assets/pikachu.jpg?resize");
4
return (
5
<amp-image-lightbox id="lightbox1" layout="nodisplay" />
6
<figure>
7
<amp-img
8
on="tap:lightbox1" role="button" tabindex="0" layout="responsive"
9
className="shirase" width={shirase.width} height={shirase.height} src={shirase.src}
10
></amp-img>
11
<figcaption>JSDF Antarctic IceBreaker Shirase</figcaption>
12
</figure>
13
<div>
14
<amp-img
15
on="tap:lightbox1" role="button" tabindex="0" layout="responsive"
16
className="pikachu" aria-describedby="imageDescription"
17
width={pikachu.width} height={pikachu.height} src={pikachu.src}
18
></amp-img>
19
<div id="imageDescription">A wild pikachu in WA.</div>
20
</div>
21
);
22
};

amp-image-slider

中央のスライダーを動かして、画像を比較できる。個人的には Photoshop での画像修正のビフォーアフターを見せる箇所の奴。画像ラベルには通常の div 要素にはない属性を必要とし、.d.ts で拡張することにした。

index.d.ts
1
import { AriaAttributes, DOMAttributes } from "react";
2
declare module "react" {
3
interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
4
first?: "";
5
second?: "";
6
}
7
}
Image from Gyazo
undefined
1
const AmpImageSlider = () => {
2
const lqip = require("@public/assets/pikachu.jpg?lqip");
3
const pikachu = require("@public/assets/pikachu.jpg?resize");
4
return (
5
<>
6
<amp-image-slider layout="responsive" width="100" height="200">
7
<amp-img
8
src={lqip.src} alt="lqip" width={pikachu.width} height={pikachu.height}
9
></amp-img>
10
<amp-img
11
src={pikachu.src} alt="pikachu" width={pikachu.width} height={pikachu.height}
12
></amp-img>
13
<div first="">this is pikachu lqip</div>
14
<div second="">this is pikachu</div>
15
</amp-image-slider>
16
</>
17
);
18
};

amp-carousel も実際に触ってみたが、controls や autoplay、loop に空文字を渡せるように.d.ts に定義する以外は真新しいものは無かったので割愛。ただ、amp-carousel に指定できる属性が多く、属性だけで見た目や動作などを大きく変えられるので、弄って遊ぶだけでも面白かった。

Image from Gyazo

others

[TOC]

Last

References

amp 化する際に、読んだり参考にしたもの。