18
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-05-05

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

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

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

スクリーンショット 2021-05-04 23.18.20.png

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

作業の流れ

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

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

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

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

環境構築

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

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

まずは.gitignoreこちらのサイトからmacos,emacs,vim,node,yarnで作成します。

Next.jsの導入

Next.jsの導入はこちらを参考にしました。

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

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

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

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

$ mkdir src/pages
$ yarn run dev

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

Typescriptの導入

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

tsconfig.jsonContentfulのNext.js対応したGithubレポジトリのものを参考にしました。

Docker導入

Dockerfiledocker-compose.ymlを作成します。

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"]
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の公式サイトを参考にしました。

$ yarn add -D -E tailwindcss postcss autoprefixer
$ npx tailwindcss init -p
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を修正しました。

$ yarn add -D -E cssnano
postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
    ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}),
  },
}

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

ESLintの導入

コードの見栄え、可読性を統一するため、こちらも公式サイトを参考にしてESLintを導入していきます。

$ 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に記載します。

.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をセットアップしていきます。

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

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

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

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

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

Contentful上でコンテンツ作成

Contentfulを初めて触るので、いろいろと下調べをしました。以下が参考にした記事URLになります。

アカウント作成

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

スクリーンショット 2021-04-29 20.32.53.png

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

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

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

コンテンツモデルの作成

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

スクリーンショット 2021-04-30 8.19.39.png

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

スクリーンショット 2021-04-30 8.20.45.png

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

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

スクリーンショット 2021-04-30 8.22.47.png

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

スクリーンショット 2021-04-30 8.23.30.png

コンテンツの作成

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

スクリーンショット 2021-04-30 8.24.15.png

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

スクリーンショット 2021-04-30 8.25.17.png

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

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

スクリーンショット 2021-04-30 8.28.18.png

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

locale設定

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

スクリーンショット 2021-04-30 9.39.57.png

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

スクリーンショット 2021-04-30 9.40.51.png

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

スクリーンショット 2021-04-30 9.41.42.png

GraphQL Content APIでのコンテンツ取得動作確認

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

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

スクリーンショット 2021-04-30 8.29.03.png

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

スクリーンショット 2021-04-30 8.29.45.png

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

スクリーンショット 2021-04-30 8.30.21.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

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

スクリーンショット 2021-04-30 8.46.27.png

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

GraphQL Content APIへの対応

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

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

ApolloClientの導入

ApolloClientの導入自体はApolloClientの公式ドキュメントを参考にしながら作業しました。また、Next.jsの公式レポジトリにApollo対応した実装例があるため、コード自体はそちらを参考にしています。

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

schema取得

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

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

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

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を取得しました。

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

graphql-codegenで型生成

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

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

生成されたcodegen.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 generateinit時に追加されたscriptが実行され、型が生成されます。

生成されたgraphql.tsxtype ScalarsDateTimeDimensionがあり、anyで型定義されています。anyを許容しない場合は、説明文を参考にしてstringnumberに変更すると良いかもしれません。

Next.jsでのサイト作成

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

local-only field対応

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

ApolloClientにはlocal-only fieldの機能があり、キャッシュ機能を用いてローカルの状態を管理することができます(Apollo公式ドキュメント)。

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

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を実行して型生成します。

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

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

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に下記のスクリプトを追加しました。

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

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

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

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を参考にreact-gtm-moduleを導入し、_app.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を参考にNext.jsをbuild, exportして、gh-pagesにpushするGithub Actionsを作成しました。

.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

上記の設定によって、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等で自身のコードがところどころ型定義の良さを使い越せなかった印象

ご参考までに。

18
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?