Next.js 10 + Tailwind CSS 2.0 でAMP対応(hybrid)を行う

Next.js 10 + Tailwind CSS 2.0 でAMP対応(hybrid)を行う
公開日時

概要

  • Next.jsで作成した既存の静的サイトをAMP対応化
  • next/ampのhybridモードを使い、通常ページとAMPページを生成
  • CSSにはTailwind CSS 2.0を使用
  • PostCSSで容量削減を行い、inline cssとして埋め込み
  • AMPページではjsが使えないので無効化した
    • GoogleAnalyticsについは <amp-analytics> を使う方法があるが、今回は対応していない

参考

既存サイトについて

Next.js 10 + Tailwind CSS 2.0で作成した静的サイト。

Static HTML Export」を用いてビルド時に静的書き出ししたものをfirebaseにデプロイしている。

next build && next export

next/ampを用いてAMP対応

next/ampを参考に、AMP対応したいページに↓を追加。

export const config = { amp: "hybrid" }

AMP化するとカスタムjsが使えなくなるが、既存ページでSNSシェアボタンをはじめカスタムjsをいくつか使用していたため、今回はhybridを指定して通常ページとAMPページをそれぞれ分けることにした。

AMP対応ページのサンプルは↓のようになる。

// pages/some.tsx
import { NextPage } from "next"
import React from "react"

import Share from "~/components/Share"

export const config = { amp: "hybrid" }

const SomePage: NextPage = () => {
  return (
    <>
      <Share url="/some" text="SomePage" />
    </>
  )
}

export default SomePage

ローカル環境を立ち上げ http://localhost:3000/some?amp=1 にアクセスするとAMP版のページが表示される。

これだけで基本的なAMP対応ができるのはとても楽。

ただし、この時点ではCSSの設定を行っていないためスタイルは崩れた状態、かつカスタムjsが有効になっているためターミナルにAMP Validationのエラーが色々と表示される。

カスタムjsの無効化

3rd Party jsの読み込みを無効化

pages/_document.tsx では this.props.inAmpMode でAMPモードの区別ができる。

これを用いてAMPモードの場合はカスタムjsを読み込まないように設定した。

import Document, { Head, Html, Main, NextScript } from "next/document"
import React from "react"

class MyDocument extends Document {
  render() {
    const isAmp = this.props.inAmpMode

    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />

          {!isAmp && (
            <>
              <script src="/some_third_party1.js"></script>
              <script src="/some_third_party2.js"></script>
            </>
          )}
        </body>
      </Html>
    );
  }
}

export default MyDocument

Componentでのjs処理を無効化

react-shareを用いてSNSシェアボタンを付けていたがAMPモードだと動かないので無効化した。

// components/Share.tsx
import React from "react"
import { useAmp } from "next/amp"
import { TwitterIcon, TwitterShareButton } from "react-share"

type Props = {
  text: string
  url: string
};

const Share: React.FunctionComponent<Props> = ({ text, url }) => {
  const isAmp = useAmp()

  if (isAmp) {
    return <></>
  }

  return (
    <div>
      <TwitterShareButton
        url={url}
        title={text}
        className="focus:outline-none"
      >
        <TwitterIcon size={32} round />
      </TwitterShareButton>
    </div>
  );
};

export default Share

同様にしてGoogleAnalyticsのEvent送信もAMPモードの場合は無効化するように設定した。

if (!isAmp) {
  sendGaEvent("some", "event")
}

これでAMP Validationのカスタムjsに関するエラーが消えた。

Tailwind CSSをinline cssとして埋め込み

AMP対応前のTailwind CSS設定

Install Tailwind CSS with Next.js」を参考に設定を行った。

tailwind.config.js は↓のように特にカスタマイズしていない状態。

// eslint-disable-next-line no-undef
module.exports = {
  purge: ["./components/**/*.tsx", "./pages/**/*.tsx"],
  darkMode: false,
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [],
};

postcss.config.js もカスタマイズしていない状態。

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

また、 css/tailwind.css もカスタムcssは追加していない状態。

@import "tailwindcss/base";

/* Start purging... */
@import "tailwindcss/components";
/* Stop purging. */

/* Start purging... */
@import "tailwindcss/utilities";
/* Stop purging. */

この状態で、 _app.tsxtailwind.css を読み込むようにしていた。

// pages/_app.tsx
import "~/css/tailwind.css"

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp

AMP対応による変更点

CSS容量削減

Post CSSで未使用のCSSを削除するためにライブラリを追加する。

yarn add -D cssnano @fullhuman/postcss-purgecss postcss-cli

postcss.config.js を↓に変更。

/* eslint-disable @typescript-eslint/no-var-requires, no-undef */
const purgecssOption = {
  content: [
    "./pages/**/*.{js,jsx,ts,tsx}",
    "./components/**/*.{js,jsx,ts,tsx}",
  ],

  defaultExtractor: (content) => content.match(/[\w-/:]+(?<!:)/g) || [],
};

module.exports = {
  plugins: [
    require("tailwindcss"),
    require("autoprefixer"),
    require("@fullhuman/postcss-purgecss")(purgecssOption),
    require("cssnano")({
      preset: "default",
    }),
  ],
};

PostCSSにpurge設定を追加したので tailwind.config.js からpurgeを削除。

// eslint-disable-next-line no-undef
module.exports = {
  darkMode: false,
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [],
};

npm scriptとしてpostcssを実行するように package.jsonbuild-css を追加。

devbuild 時に build-css を実行するように設定。

{
  "scripts": {
    "dev": "yarn build-css && next dev",
    "build": "yarn build-css && next build && next export",
    "build-css": "postcss css/tailwind.css --config postcss.config.js -o css/output.css",
    "start": "next start",
  }
}

これで未使用のCSSを削除したものが css/output.css として書き出される。

容量を確認してみると 9KB に削減できていた。

output.cssをinline cssとして埋め込む

raw-loaderを追加。

yarn add -D raw-loader

next.config.js のwebpack設定に raw-loader を追加してcssファイルを文字列としてインポートできるようにする。

/* eslint-disable @typescript-eslint/no-var-requires, no-undef */
module.exports = {
  webpack: (config) => {
    config.module.rules.push({
      test: /\.css$/,
      use: "raw-loader",
    });

    return config;
  },
}

続いて pages/_document.tsx でoutputcssを読み込み、getInitialPropsでinline cssとして埋め込む。

/* eslint-disable @typescript-eslint/ban-ts-comment */
import Document, { Head, Html, Main, NextScript } from "next/document"
import React from "react"
// @ts-ignore
import outputcss from "!raw-loader!../css/output.css"

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx)
    return {
      ...initialProps,
      styles: (
        <>
          {initialProps.styles}
          <style
            dangerouslySetInnerHTML={{
              __html: outputcss,
            }}
          />
        </>
      ),
    }
  }

  render() {
    const isAmp = this.props.inAmpMode

    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />

          {!isAmp && (
            <>
              <script src="/some_third_party1.js"></script>
              <script src="/some_third_party2.js"></script>
            </>
          )}
        </body>
      </Html>
    )
  }
}

export default MyDocument

inline css埋め込みを行ったので pages/_app.tsx でimportしていた tailwind.css は削除しておく。

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp

これで yarn dev を実行すると ?amp=1 のページにもCSSが適用されていることが確認できる。

build & export

一通りAMP対応ができたので静的書き出しをして最終確認を行う。

yarn build
yarn start

pages/some.tsx をhybrid AMP対応にしていた場合、↓でそれぞれのページにアクセスできる。

  • 通常ページ: http://localhost:3000/some/
  • AMPページ: http://localhost:3000/some.amp/

通常ページには AMPページの存在を示す amphtml タグが自動で埋め込まれている。

<link rel="amphtml" href="/some.amp">

また、AMPページには canonical タグが自動で埋め込まれる。

<link rel="canonical" href="/some" />

なお、独自に canonical タグを設定している場合は、独自設定が優先して埋め込まれていた。

スタイル崩れ等が起きていないことが確認できたらデプロイを行う。

AMP テスト

AMP テスト - Google Search Console」にデプロイ後のAMPページURLを入力してAMPページに問題がないことを確認する。

AMPのindex登録について

AMP対応ページ(/some.amp/)は sitemap.xml に追加する必要はないとのこと。

rel=amphtml が記載されていればAMPページも参照してくれる模様。

Search Console側は AMP ステータス レポート から確認できるが、Search Consoleに反映されるには数日かかりそうなので、しばらく待ってから再確認しよう。

ひとまずこれでNext.js 10 + Tailwind CSS 2.0環境でのAMP対応ができた。


Related #next.js

SharedArrayBuffer updates in Android Chrome 88 and Desktop Chrome 92

クロスオリジン分離対応を実施

react-hook-formとReact Datepickerを組み合わせる

Hook FormのControllerを使う

Next.jsで生成したサイトで特定のページのみnoindexを設定する

タグに紐づく記事一覧ページはnoindexにした

Next.jsでAdsenseタグを埋め込んだら Only one AdSense head tag supported per page エラーが発生

Only one AdSense head tag supported per page. The second tag is ignored.