Contentful, Next.js, Apolloで作成したポートフォリオをGithub Pagesに公開したときの備忘録

HeadlessCMSやNext.jsのSSR/SSGの知見を得るべく、ContentfulのGraphQL Content APIからデータ取得して、Next.jsでサイトを作成してみました。

一連の作業ログをまとめましたので、今後同じようにサイトを作成したい人は参考にしてください。

作成したものはGithub Pagesにデプロイしているので閲覧いただけます。

https://adacchi3.github.io/

![スクリーンショット 2021-05-04 23.18.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206630/2f0bc163-0027-d7d1-6b1f-47b270ceab2e.png)

また、ソースコード郡はこちらを参考にしてください。

https://github.com/Adacchi3/adacchi3.github.io

# 作業の流れ

実際に作業した流れとしては、下記のとおりになります。

1. 作業手順・内容の調査・確認
1. 環境構築
    1. Next.jsの導入
    1. Typescriptの導入
    1. Dockerの導入
    1. TailwindCSSの導入
    1. ESLintの導入
    1. huskyの導入
1. Contentful上でコンテンツ作成
    1. アカウント作成
    1. コンテンツモデルの作成
    1. コンテンツの作成
    1. locale設定
    1. GraphQL Content APIでのコンテンツ取得動作確認
1. GraphQL Content APIへの対応
    1. ApolloClientの導入
    1. schema取得
    1. graphql-codegenで型生成
1. Next.jsでのサイト作成
    1. local-only field対応
    1. SSG対応
    1. Google Tag Manager対応
1. Github Actionsの作成・デプロイ

作業手順・内容の調査・確認では、作業の概観を把握するためにいろいろな記事から作業内容を調査しました。各項目ごとに参考にした記事URLを貼っておきます。

では、１つずつ作業内容を記していきます。

# 環境構築
ローカル環境のyarn, nodeのバージョンは以下のとおりです。

```shell
$ yarn -v
1.22.10
$ node -v
v14.16.1
```

まずは`.gitignore`を[こちらのサイト](https://www.toptal.com/developers/gitignore)から`macos,emacs,vim,node,yarn`で作成します。

## Next.jsの導入
Next.jsの導入は[こちら](https://qiita.com/tetsutaroendo/items/31300a5e475688a2f4ec)を参考にしました。

```sh
$ yarn init -v
# package.jsonにてprivate: true を入れておく
$ yarn add -E react react-dom next
```

続いて、package.jsonに以下のスクリプトを追加します。

```sh:package.json
...
"scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
  },
...
```

Next.jsは`pages`ディレクトリを参照してページを作成するので、適宜`pages`ディレクトリを作成して、`yarn run dev`で動作確認します。

```sh
$ mkdir src/pages
$ yarn run dev
```

その後、localhost:3000にアクセスして、404ページが出力されることを確認しました。

## Typescriptの導入

```sh
$ yarn add -D typescript @types/react -E
$ npx tsc --init
```

`tsconfig.json`は[ContentfulのNext.js対応したGithubレポジトリのもの](https://github.com/contentful/compose-starter-helpcenter-nextjs/blob/master/tsconfig.json)を参考にしました。

## Docker導入

`Dockerfile`と`docker-compose.yml`を作成します。

```dockerfile:Dockerfile
FROM node:14.16.1-alpine

ENV TZ=Asia/Tokyo
WORKDIR /app

COPY package.json /app
COPY yarn.lock /app

RUN yarn install

COPY . /app

CMD ["yarn", "dev"]
```

```yml:docker-compose.yml
version: '3'
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    command: 'yarn run dev'
    ports:
      - 3000:3000
    volumes:
      - .:/app:cached
      - node_modules:/app/node_modules
    tty: true
    stdin_open: true
    environment:
      # 適宜環境変数を追加していく
volumes:
  node_modules:
```

`node_modules`等、イメージをビルドする際に処理が遅くなってしまうようなファイル郡は`.dockerignore`で除外します。

```:.dockerignore
node_modules
yarn-error.log
.husky
.next
.git
out
```

その後、`$ docker-compose up --build`を実行した後、localhost:3000にアクセスして404ページを閲覧できるのを確認しました。

## TailwindCSSの導入

CSSでのマークアップはTaliwindCSSを使いました。CSSプロパティがクラス名とマッピングされており、私個人としては使いやすい印象を持っています。都度コンポーネントのクラス名を考えなくて良いのもポイントです。

導入にあたっては、[TailwindCSSの公式サイト](https://tailwindcss.com/docs/guides/nextjs)を参考にしました。

```sh
$ yarn add -D -E tailwindcss postcss autoprefixer
$ npx tailwindcss init -p
```

```js:tailwind.config.js
module.exports = {
  mode: 'jit',
  purge: ['./src/pages/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}'],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}
```

また、デプロイ時にCSSファイルをminifyするべく、`cssnano`を導入し、`postcss.config.js`を修正しました。

```sh
$ yarn add -D -E cssnano
```

```js:postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
    ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}),
  },
}
```

各コンポーネントへTailwindCSSの適用は、Next.jsで`pages`ディレクトリ直下に作成する`_app.tsx`にて`import 'tailwindcss/tailwind.css'`を記載します。詳しくは[こちら](https://tailwindcss.com/docs/guides/nextjs)を参考にしてください。

## ESLintの導入

コードの見栄え、可読性を統一するため、こちらも[公式サイト](https://eslint.org/docs/user-guide/getting-started)を参考にしてESLintを導入していきます。

```sh
$ yarn add -E -D eslint eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-tsc eslint-plugin-tailwindcss @typescript-eslint/parser @typescript-eslint/eslint-plugin prettier eslint-config-prettier eslint-plugin-prettier
$ npx eslint --init
```

その後、各種プラグインの設定やルールを`.eslintrc.json`に記載していきます。ESLintから除外したいファイルがある場合は`.eslintignore`に記載します。

```json:.eslintrc.json
{
  "env": {
    "browser": true,
    "node": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:prettier/recommended"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": 12,
    "sourceType": "module"
  },
  "plugins": ["@typescript-eslint", "react-hooks", "tailwindcss", "tsc"],
  "settings": {
    "react": {
      "version": "latest"
    }
  },
  "rules": {
    "eol-last": ["error", "always"],
    "react/prop-types": [0],
    "tailwindcss/classnames-order": 2,
    "tailwindcss/no-custom-classname": 2,
    "tailwindcss/no-contradicting-classname": 2,
    "prettier/prettier": [
      "warn",
      {
        "tabWidth": 2,
        "semi": false,
        "singleQuote": true
      }
    ]
  }
}
```
```:.eslintignore
node_modules
.next
next.config.js
out
```

ESLintを設定した後に`package.json`のscriptsに`"lint": "eslint --ext .js,.jsx,.ts,.tsx,.json ."`を追加した後に、`$ yarn run lint --fix`してルールを適用しました。

## huskyの導入

gitのコミット時にlintを走るよう、husky, lint-stagedをセットアップしていきます。

```sh
$ yarn add -D -E husky lint-staged
$ npx husky install
```

[huskyのドキュメント](https://github.com/typicode/husky#usage)では`npm set-script`を使うと書かれていますが、npm7系からのコマンドだったため、自前で導入していきます。`package.json`のscriptsに`"prepare": "husky install"`を追加しました。

`$ npx husky add .husky/pre-commit "npx lint-staged"`でhusky側の対応した後、`package.json`にlint-stagedの対応していきます。

```json:package.json
...
  "lint-staged": {
    "*.{ts,tsx,json}": "yarn run lint"
  }
...
```

以上が開発環境の構築になります。

# Contentful上でコンテンツ作成
Contentfulを初めて触るので、いろいろと下調べをしました。以下が参考にした記事URLになります。

* https://satoruakiyama.com/blog/building-blog-with-nextjs-and-contentful-ja
* https://www.contentful.com/developers/docs/references/graphql/

## アカウント作成

Contentfulにアクセスして、アカウントを作成します。自分はGithubアカウントでSIGN UPしていきます。

![スクリーンショット 2021-04-29 20.32.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206630/9a5c4fab-22c5-74c8-d41f-556f46e5760a.png)

指示に従って、入力・規約の確認を行い、アカウントを作成します。今回はフリープランで紹介していきます。

その後、Organization名を入力し、spaceを作成します。個人作成の場合は個人だとわかるよう、Organization名を設定し、space名は使用するサイト名などにしておくと把握しやすいかと思います。

私の場合は、Organization: Adacchi3, space: adacchi3 portfolioにしました。

## コンテンツモデルの作成

コンテンツを作成するにあたり、コンテンツモデルを作成していきます。いわゆるDBスキーマ作成のようなものになります。下記画像のヘッダー部分にあたるContent modelをクリックします。

![スクリーンショット 2021-04-30 8.19.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206630/a32386dd-adad-9ef5-2f60-d5b920669a39.png)

新規コンテンツモデルを作成する場合は、下記画像右上にあるAdd content typeをクリックします。

![スクリーンショット 2021-04-30 8.20.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206630/44b1fac2-5431-1755-6867-7caf5358915e.png)

コンテンツタイプ名は`Post`や`User`のようなクラスに該当する名称になります。下記画像の場合は`Achievement`になります。コンテンツタイプのフィールド（プロパティ・属性に該当するもの）を追加していく場合は下記画像の右上にあるAdd fieldを選択していきます。

フィールドに選択できる型としてはShort textやDate & Time, Integerと幅広い種類があります。また、Short textの中でもURLといったようなバリデーションをつけることができます。

![スクリーンショット 2021-04-30 8.22.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206630/d59b8d73-9800-ad9b-a160-4b52da38854b.png)

フィールドの型の１つにReferenceというものがあります。こちらはいわゆるコンテンツとコンテンツを関連させることができ、has_on, has_manyの関連設定もできます。バリデーションを設定しない場合はあらゆるコンテンツタイプと紐付けることができますが、制限したい場合は下記画像のような設定項目によって一意のコンテンツタイプとの関連を設定することができます。

![スクリーンショット 2021-04-30 8.23.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206630/5c15b3e9-1478-3dfd-ae79-7a4bb5fa9b4b.png)

## コンテンツの作成

続いて、コンテンツ（エントリー）を作成していきます。いわゆる、レコード・インスタンスに該当するデータそのものになります。下記画像が示すヘッダーのcontentをクリックします。

![スクリーンショット 2021-04-30 8.24.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206630/16b1d2ed-09db-478e-abfd-3539afd78755.png)

エントリーを新規作成する場合は右上にあるAdd Entryをクリックし、コンテンツタイプを選択し、データを入力していきます。

![スクリーンショット 2021-04-30 8.25.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206630/0641d497-c3b3-9c0e-08b9-3103ea9a672a.png)

入力フォームは下記画像のとおりです。Referenceと設定したフィールドでは、Add existing contentを選択することで、すでに作成済みのコンテンツを選ぶことができます。また、NEW CONTENTの下にあるコンテンツタイプを選択することで、新規作成もできます。

Referenceをhas_manyにした場合は、コンテンツを複数選択することができます。その際、選択したコンテンツの順序を入れ変えることができ、データ取得時もその順序は引き継がれます。

![スクリーンショット 2021-04-30 8.28.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206630/8af4762a-43d7-ffdf-9cbb-b66252f0df8e.png)

STATUSはそれぞれDraft, Changed, Published, Archivedの4つがあり、preview時のみ閲覧できるコンテンツ、本番反映するコンテンツなどの設定ができます。

## locale設定

Contentfulではコンテンツの多言語対応もできます。ここでは、デフォルトの言語設定を見ていきます。下記画像のヘッダーから設定を選び、Localesをクリックします。

![スクリーンショット 2021-04-30 9.39.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206630/25999102-c621-9d4c-7484-7c46efefa996.png)

デフォルトになっている言語をクリックします。

![スクリーンショット 2021-04-30 9.40.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206630/9730a1da-cc3f-b662-65e7-001f0cf01e8a.png)

プルダウンメニューからお好みの言語を選択してください。デフォルトでは、Locale settingsは変更できないですが、オプションで追加する言語については設定を見直すことができます。フリープランでは2言語まで対応しているようです。

![スクリーンショット 2021-04-30 9.41.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206630/c4e0533b-57f3-61d9-a81a-cb4419830c1d.png)


## GraphQL Content APIでのコンテンツ取得動作確認
作成したコンテンツが実際に取得できるかをGraphQL Content APIを通じて確認していきます。

はじめに、API keyを作成していきます。

![スクリーンショット 2021-04-30 8.29.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206630/183b36e7-3f3d-5f3c-b3f3-cda6032391f7.png)

Content delivery/preview tokensとContent management tokensの２つ作成することができます。今回はデータ取得のため、Content delivery/preview tokensのタブを選択した後に、Add API keyをクリックします。

![スクリーンショット 2021-04-30 8.29.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206630/be7da496-1aa9-7748-a9b4-a0eba35fceed.png)

作成すると、SpaceIDとContent Delivery API, Content Preview APIの３つ取得できます。[Contentfulの公式GraphQLドキュメント](https://www.contentful.com/developers/docs/references/graphql/)で示されているCDA_TOKENはContent Delivery API - access tokenのことを指しています。Content Delivery API, Content Preview APIの違いとしては、publishedステータスのコンテンツのみが取得できるか・preview設定を有効にした際にDraftのコンテンツを取得できるかどうかだと思われます。開発環境(またはプレビュー)と本番環境での使い分けをしていく必要がありそうです。

![スクリーンショット 2021-04-30 8.30.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206630/d335841d-4ae7-ae90-cabc-76c1d81a0b19.png)

発行したCDA_tokenを使って、Contentfulが提供しているGraphiQLにアクセスしてみます。URLは`https://graphql.contentful.com/content/v1/spaces/{SPACE}/explore?access_token={CDA_TOKEN}`と適宜SPACEとCDA_TOKENを置換してください。

GraphiQLで表示されるドキュメントを確認しながら、エディタ上でクエリを作成して実行してみてください。

![スクリーンショット 2021-04-30 8.48.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206630/9b5c8a23-5fb8-30ea-9c44-6bfa89a1ffa9.png)

Chrome拡張のAltairなどで確認したい場合はGraphQLのエンドポイントを`https://graphql.contentful.com/content/v1/spaces/{SPACE}/environments/{ENVIRONMENT}?access_token={CDA_TOKEN}`を指定すると確認できます。

![スクリーンショット 2021-04-30 8.46.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206630/2d44b6e7-b0e6-676b-b1b6-86b5ac3891ce.png)

以上で、Contentful上でのコンテンツ作成は完了です。

# GraphQL Content APIへの対応

Next.jsからContentfulのGraphQL Content APIからデータ取得できるよう対応していきます。

以下の記事URLを参考にさせていただきました。

* https://www.apollographql.com/docs/react/get-started/
* https://github.com/vercel/next.js/tree/canary/examples/with-apollo
* https://github.com/apollographql/apollo-tooling
* https://qiita.com/koedamon/items/0fd3a01f7e398e54b747
* https://www.graphql-code-generator.com/docs/getting-started/installation
* https://qiita.com/mizchi/items/fb9f598cea94d2c8072d

## ApolloClientの導入
ApolloClientの導入自体は[ApolloClientの公式ドキュメント](https://www.apollographql.com/docs/react/get-started/)を参考にしながら作業しました。また、[Next.jsの公式レポジトリにApollo対応した実装例](https://github.com/vercel/next.js/tree/canary/examples/with-apollo)があるため、コード自体はそちらを参考にしています。

Next.jsのSSGを用いる場合、Next.jsの公式レポジトリのApollo対応した実装例を参考にすることが必要になります。実装例ではキャッシュ機構を有効に活用した実装になってます。`getStaticProps`上で取得したクエリ結果をキャッシュし、それぞれのコンポーネントでクエリを発行した場合はキャッシュしたクエリ結果を使うようになっているようです（[参考](https://qiita.com/madogiwa/items/27c5b71ee6038803e5e3)）。

## schema取得
今回はTypescriptでの実装であるため、Contentfulで作成したコンテンツタイプの型定義をする必要があります。ただ、自前で作成するのはコンテンツタイプの量と比例して作業時間も増えてしまうため、自動で生成できるようにしていきます。

本来であれば`graphql-codegen`でバシっと作るのが理想ですが、クエリを更新するたびにGraphQL Content APIにアクセスするのが忍びなかったため、apollo toolでschemaをダウンロードすることにしました。

`$ yarn add -D -E apollo`した後、下記のファイルを作成します。`.env`にGraphQL Content APIのURLを記載しておきます。

```js:apollo.config.js
module.exports = {
  client: {
    name: 'client',
    includes: ['src/graphql/schema/**/*.{ts,tsx,graphql}'],
    excludes: [
      'src/graphql/queries/*.{ts,tsx,graphql}',
      'src/graphql/generated/*.{ts,tsx,graphql}',
      'src/graphql/graphql.schema.json',
    ],
    addTypename: true,
    service: {
      name: 'contentful graphql endpoint',
      url: process.env.GRAPHQL_ENDPOINT,
    },
  },
}
```

`package.json`のscriptsに`"apollo:download": "apollo client:download-schema src/graphql/schemas/contentful/schema.graphql"`を追加します。

その後は以下のコマンドを実行して、schemaを取得しました。

```sh
$ docker-compose run --rm app /bin/sh
/app # yarn run apollo:download
/app # exit
```

## graphql-codegenで型生成
まずはgraphql-codegenを導入していきます。`init`の際にいろいろと質問されますが、該当する選択肢を選べば大丈夫だと思います。

```sh
$ yarn add -D -E @graphql-codegen/cli
$ yarn graphql-codegen init
```

生成された`codegen.yml`を以下のように修正しました。

```yml:codegen.yml
overwrite: true
schema: "src/graphql/schemas/**/schema.graphql"
documents: "src/graphql/queries/*.graphql"
generates:
  src/graphql/generated/graphql.tsx:
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-react-apollo"
    config:
      withHOC: false
      withHooks: true
      withComponents: false
  src/graphql/graphql.schema.json:
    plugins:
      - "introspection"
```

その後、`$ yarn run generate`で`init`時に追加されたscriptが実行され、型が生成されます。

生成された`graphql.tsx`の`type Scalars`に`DateTime`や`Dimension`があり、`any`で型定義されています。`any`を許容しない場合は、説明文を参考にして`string`や`number`に変更すると良いかもしれません。

# Next.jsでのサイト作成
Next.jsでpagesやcomponentsを作成していきます。ここでは留意した点を記載していきます。

## local-only field対応
今回のサイト要件では、多言語対応（日本語・英語）に対応する必要がありました。Next.jsにはlocalesの機能がありますが、SSGには対応していなかったため、自前で対応することになりました。

ApolloClientにはlocal-only fieldの機能があり、キャッシュ機能を用いてローカルの状態を管理することができます（[Apollo公式ドキュメント](https://www.apollographql.com/docs/react/local-state/managing-state-with-field-policies/)）。

ApolloClientを設定しているファイル上に下記のような実装を施しました。`makeVar`で管理したい状態（今回の場合は多言語設定）を定義し、`InMemoryCache`ではfieldからlocaleを呼び出した際に`makeVar`で定義した状態を参照するよう設定します。

```tsx:src/client/index.tsx
import { ApolloClient, HttpLink, InMemoryCache, makeVar } from '@apollo/client'
...
export const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        locale: {
          read() {
            return localeVar()
          },
        },
      },
    },
  },
})
...
export const localeVar = makeVar('ja-JP')
```

次にローカルの状態を参照するGraphQLのクエリ/スキーマを作成し、`$ yarn run generate`を実行して型生成します。

```graphql:src/graphql/queries/locale.graphql
query Locale {
  locale @client
}
```
```graphql:src/graphql/schemas/local/schema.graphql
type Query {
  locale: String!
}
```

その後、localeの状態を参照できるようカスタムフックを作成しました。

```tsx:src/hoooks/Locale.tsx
import { useLocaleQuery } from '@graphql/generated/graphql'

export const useLocale = (): string => {
  const { data: localeData } = useLocaleQuery()
  return String(localeData?.locale)
}
```

状態の変更は各ページにて、`localeVar('en-US')`等で変更しています。今回のページ構成では、`/`は日本語、`/en/`は英語で設計したため、このような仕様で落ち着きました。

## SSG対応
ホスティングサービスをこれまで通り、Github Pagesを使おうとするとSSRやISRは対応できず、SSG一択になりました。（SSRやISRを使う場合はVercelがおすすめらしいです）

Next.jsのSSGする場合、各種fetchするデータは`getStaticProps`内に処理を記述する必要があります。処理自体はNext.jsの実装例を参考にしながら作成していきました。

ここの詳細は検索するといろいろと出てきますので、SSGの実装内容については割愛します。

今回はGithub Pagesにデプロイするため、実際に生成されるSSGを確認するために、`serve`を導入して動作確認を行いました（ここの動作確認はDocker上ではなくローカル環境上で行ってました）

`$ yarn add -D -E serve`を実行し、package.jsonのscriptsに下記のスクリプトを追加しました。

```json:package.json
...
    "export": "next export",
    "serve": "yarn run build && yarn run export && serve ./out",
...
```

その後、`$ yarn run serve`を実行することで、localhost:5000で実際にSSGされたページにアクセスすることができます。

`next build`時には以下のようなログが確認することができました。SSGされたページの確認等ができるのが良い点だなと思いました。

```sh
Page                              Size     First Load JS
┌ ● /                             525 B           214 kB
├   /_app                         0 B             115 kB
├ ○ /404                          3.29 kB         118 kB
├ ○ /articles                     1.09 kB         131 kB
├ ● /articles/[slug]              1.16 kB         131 kB
├   ├ /articles/hoge
├   └ /articles/fuga
└ ● /en                           527 B           214 kB
+ First Load JS shared by all     115 kB
  ├ chunks/408.7f9089.js          49.1 kB
  ├ chunks/commons.4d71e3.js      13.2 kB
  ├ chunks/framework.9c2f4b.js    42 kB
  ├ chunks/main.7c63f6.js         7.16 kB
  ├ chunks/pages/_app.712491.js   2.02 kB
  ├ chunks/webpack.e76463.js      1.14 kB
  └ css/1d7a68075e1fa382472f.css  2.57 kB

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)
   (ISR)     incremental static regeneration (uses revalidate in getStaticProps)
```

## GTM対応
サイトの計測のため、Google Tag Managerを導入しました。

[こちらの記事URL](https://www.learnbestcoding.com/post/9/easiest-way-to-integrate-google-analytics-with-react-js-and-next-js)を参考に`react-gtm-module`を導入し、`_app.tsx`内にて設定しました。

```tsx:src/pages/_app.tsx
import React, { useEffect } from 'react'
import { AppProps } from 'next/app'
import { ApolloProvider } from '@apollo/client'
import { useApollo } from '@client'
import 'tailwindcss/tailwind.css'
import TagManager from 'react-gtm-module'

const MyApp: React.FC<AppProps> = ({ Component, pageProps }: AppProps) => {
  const apolloClient = useApollo(pageProps)

  useEffect(() => {
    TagManager.initialize({ gtmId: String(process.env.GTM_ID) })
  }, [])

  return (
    <ApolloProvider client={apolloClient}>
      <Component {...pageProps} />
    </ApolloProvider>
  )
}

export default MyApp
```

Single Page ApplicationのPage Viewを計測するためには、GTM上のトリガーで「履歴の変更」を作成し、適用する必要があります。こちらは「SPA GTM」で検索すると記事がヒットするのでご確認ください。

# Github Actionsの作成・デプロイ

[こちらの記事URL](https://qiita.com/manten120/items/87e9e822800403904dc8)を参考にNext.jsをbuild, exportして、gh-pagesにpushするGithub Actionsを作成しました。

```yml:.github/workflows/gh-pages.yml
name: deploy github page
on:
  push:
    branches:
      - master
jobs:
  deploy:
    name: next build, export and deploy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: setup node
        uses: actions/setup-node@v2
        with:
          node-version: '14'
      - name: get yarn cache directory path
        id: yarn-cache-dir-path
        run: echo "::set-output name=dir::$(yarn cache dir)"
      - name: cache dependencies
        uses: actions/cache@v2
        id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
        with:
          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-
      - name: install dependencies
        run: yarn install --prefer-offline
      - name: build
        env:
          GRAPHQL_ENDPOINT: ${{secrets.GRAPHQL_ENDPOINT}}
          AUTHOR_ID: ${{secrets.AUTHOR_ID}}
          GTM_ID: ${{secrets.GTM_ID}}
        run: yarn run build
      - name: export
        run: yarn run export
      - name: add nojekyll
        run: touch ./out/.nojekyll
      - name: deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./out
```

このとき、環境変数はSetting→Secrets→New repository secretで設定します。

![スクリーンショット 2021-05-05 19.07.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206630/f9842ee4-95ec-fe60-fc37-cafdbca994cb.png)

上記の設定によって、masterにpushした際にgh-pagesにSSGされたファイル郡がpushされるようになりました。

Github Pagesの設定で、Source branchをgh-pagesに変更し、ディレクトリを`/(root)`にすることで、gh-pagesのファイル郡を参照してくれるようになります。

# まとめ

今回はContentful, Next.js, ApolloClient, TailwindCSSを使って、ポートフォリオサイトを作成し、Github ActionsでGithub Pagesに公開するまでの流れを示しました。

所感としては以下のとおりです。

* webpack.config.jsを書く覚悟でいたが、Next.js側でよしなにしてくれてとても助かった
    * Webpack5で動いてました、すごい！
* Next.jsでのSSG/SSR/ISRの対応方法をだいたい把握することができた
* ApolloClientのlocal-only fieldの概要を掴むことができた
    * 状態管理がApolloClientで一括管理できるのは良い
* graphql-codegenが使いやすかった
* Contentfulのコンテンツタイプの定義が難しい
    * 何も考えずに作ると無法地帯になりそう
    * ちゃんとモデル設計するのがベター
* GraphQL Content APIから取得したスキーマからgraphql-codegenで生成した型が使いづらかった
    * `Maybe<string> | undefined`等で自身のコードがところどころ型定義の良さを使い越せなかった印象

ご参考までに。