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 仕様

<!doctype html>
<html >
<head>
<meta charset="utf-8">
<title>Sample document</title>
<link rel="canonical" href="./regular-html-version.html">
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<style amp-custom>h1 {color: red}</style>
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "NewsArticle",
"headline": "Article headline",
"image": [
"thumbnail1.jpg"
],
"datePublished": "2015-02-05T08:00:00+08:00"
}
</script>
<script async custom-element="amp-carousel" src="https://cdn.ampproject.org/v0/amp-carousel-0.1.js"></script>
<script async custom-element="amp-ad" src="https://cdn.ampproject.org/v0/amp-ad-0.1.js"></script>
<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>
<script async src="https://cdn.ampproject.org/v0.js"></script>
</head>
<body>
<h1>Sample document</h1>
<p>Some text<amp-img src=sample.jpg width=300 height=300></amp-img></p>
</body>
</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 で自動最適化
  • ユーザー用の最適化済みページと、検索エンジン用のインデックス可能な非最適化ページを生成
export const config = { amp: true }
const Component = () => {
return <h3>My AMP About Page!</h3>
}
export default Component

Hybrid AMP Page

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

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

import { useAmp } from "next/amp";
export const config = { amp: "hybrid" };
const Component = () => {
const isAmp = useAmp();
return (
<>
{isAmp ? (
<amp-img
layout="responsive"
width="300"
height="300"
src="/my-img.jpg"
alt="a cool image"
/>
) : (
<img width="300" height="300" src="/my-img.jpg" alt="a cool image" />
)}
</>
);
};
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

なので、このstack overflowを見ながら、custom types を追加してくれと、Next.js 公式はドキュメントで言っている。。

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

declare namespace JSX {
type ReactAmp = React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement>,
HTMLElement
>;
interface AmpImg extends ReactAmp {
children?: React.ReactNode;
alt?: string;
attribution?: string;
src?: string;
srcset?: string;
width?: string;
height?: string;
sizes?: string;
heights?: string;
layout?: "fill" | "fixed" | "fixed-height" | "flex-item" | "intrinsic" | "nodisplay" | "responsive";
fallback?: "";
on?: string; // amp-image-lightbox
role?: string;
tabindex?: string;
}
interface IntrinsicElements {
"amp-img": AmpImg;
}
}

amp-img

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

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

const AmpImg = () => {
// below is related to next-optimized-images
const image = require("@public/assets/shirase.jpg?resize");
const webp = require("@public/assets/shirase.jpg?resize&format=webp");
return (
<amp-img alt="shirase" layout="responsive"
width={webp.width} height={webp.height} src={webp.src} srcset={webp.srcSet}
>
<amp-img fallback="" alt="shirase"
width={image.width} height={image.height} src={image.src} srcset={image.srcSet}
></amp-img>
</amp-img>
);
};

amp-image-lightbox

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

const AmpImageLightbox = () => {
const shirase = require("@public/assets/shirase.jpg?resize");
const pikachu = require("@public/assets/pikachu.jpg?resize");
return (
<amp-image-lightbox id="lightbox1" layout="nodisplay" />
<figure>
<amp-img
on="tap:lightbox1" role="button" tabindex="0" layout="responsive"
className="shirase" width={shirase.width} height={shirase.height} src={shirase.src}
></amp-img>
<figcaption>JSDF Antarctic IceBreaker Shirase</figcaption>
</figure>
<div>
<amp-img
on="tap:lightbox1" role="button" tabindex="0" layout="responsive"
className="pikachu" aria-describedby="imageDescription"
width={pikachu.width} height={pikachu.height} src={pikachu.src}
></amp-img>
<div id="imageDescription">A wild pikachu in WA.</div>
</div>
);
};

amp-image-slider

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

import { AriaAttributes, DOMAttributes } from "react";
declare module "react" {
interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
first?: "";
second?: "";
}
}

Image from Gyazo

const AmpImageSlider = () => {
const lqip = require("@public/assets/pikachu.jpg?lqip");
const pikachu = require("@public/assets/pikachu.jpg?resize");
return (
<>
<amp-image-slider layout="responsive" width="100" height="200">
<amp-img
src={lqip.src} alt="lqip" width={pikachu.width} height={pikachu.height}
></amp-img>
<amp-img
src={pikachu.src} alt="pikachu" width={pikachu.width} height={pikachu.height}
></amp-img>
<div first="">this is pikachu lqip</div>
<div second="">this is pikachu</div>
</amp-image-slider>
</>
);
};

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

Image from Gyazo

others

[TOC]

Last

References

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