WordPress製ホームページをGatsby+microCMS+Netlifyに移行してみた

プライベートでやっているバンドのホームページをレンタルサーバー+WordPressで運営していたのですが、最近Gatsby+microCMS+NetlifyのいわゆるJAMstack構成のホームページにリプレースしました。

ホームページ：[男子校 Official Web Site](https://danshikoh.com/)

# 概要

今までかれこれ5年くらいレンタルサーバー+WordPressで運営してきていたのですが、

- サーバー代が毎年かかる
- 表示が重い
- 「固定ページやフォームをちょっとだけ動的にしたい」みたいなことが難しい
- プラグイン・バージョンのアップデートといった管理が面倒

といった点が課題でした。

これらの課題を解消したく、また最近気になっていたGatsbyを使ってみたかったのもあってホームページをリプレースしてみました。

# 構成

TypeScript+React+Gatsbyでコードを作成、microCMSでデータを管理してNetlifyでホスティングするという構成にしました。いわゆるJAMstackと呼ばれている構成で、動的なWebサーバーを必要とせず静的コンテンツを高速配信することができます。

## [Gatsby](https://www.gatsbyjs.org/)

React製のSSG（静的サイトジェネレーター）。MarkdownファイルやHeadless CMSと組み合わせてブログ等のサイトを簡単に作成でき、かつ爆速なサイトが簡単に作れると評判です。
「静的」とは言っていますがReactの機能はフルに使えるので、フロントエンドをフルに活用したサイトを作ることができます。

## [microCMS](https://microcms.io/)

日本製のHeadless CMS。
個人的には管理画面がシンプルで一番使いやすそうと感じたので採用しました。特にバンドのホームページは自分以外も更新するので、直感的に記事を作成できるのは大きなメリットです。もちろん全て日本語なのもGood。
Gatsbyには[対応プラグイン](https://www.gatsbyjs.org/packages/gatsby-source-microcms/)があるので、これを使用することでGraphQLクエリで簡単にデータを取得できます。
無料でも10個までAPIを作成できるので、ブログ記事以外にも曲情報やライブ情報といったAPIを作成してホームページ内で利用しています。

## [Netlify](https://www.netlify.com/)

デプロイ先の静的ホスティングサービス。静的サイト・サーバーレスに特化したサービスとなっていて、以下のような機能を提供してくれます。

- GitHubやHeadless CMSと連携した自動ビルド（プライベートリポジトリでも可）
- フォーム機能
- AWS Lambdaベースのfunctions機能

microCMSにはNetlifyとWebhook連携できる機能があるので、microCMSで記事の作成・更新時にNetlifyに通知し、更新後のコンテンツで自動ビルドすることができます。

# 効果

リプレースによって上にあげた課題がかなり改善しました。

- サーバー代が毎年かかる
    - → Netlify / microCMSともにに無料枠で十分なのでサーバー代は0になり、ドメイン代だけで済むようになった
- 表示が重い
    - → GatsbyやNetlify CDNのおかげでかなり爆速に
- 「固定ページやフォームをちょっとだけ動的にしたい」みたいなことが難しい
    - → Reactをフルに利用できるので動的処理も自由に入れられる
        - 例えば「ライブ情報から、未来の日付のライブだけ予約できるフォームを自動で作る」のようなこともWordPressでは難しかったのが簡単にできた
- プラグイン・バージョンのアップデートといった管理が面倒
    - → GitHub / npmで一括管理[^1]
        - セキュリティアップデートなんかはGitHubが自動でプルリクを生成してくれたりする

Gatsbyの使い方もReactが分かっていれば割ととっつきやすく[^2]、Gatsbyを初めて触ってからだいたい1ヶ月くらいでリプレースできました。

[^1]: GatsbyもWordPressと似たようにプラグインで機能拡張していくのでそこまで劇的に改善した訳ではないですが、WordPressのダッシュボードにログインする度にアップデートしなきゃ…となるのが無くなっただけでかなり快適です
[^2]: GraphQLが最初はとっつきにくいように思いますが、developmentサーバを立てたときに同時にGraphQLのGUI環境も立ち上がるので割と親切で分かりやすいです

# ポイント

主にGatsbyまわりのちょっとしたポイントやハマった点をいくつか紹介しようと思います。
（具体的な環境構築の手順等は今回はあまり触れません）

## デザインライブラリ

1からデザインを作成するのは結構大変だったので、Reactのマテリアルデザインライブラリ[material-ui](https://material-ui.com/ja)を使用しました。
Gatsbyで使用する場合は、[gatsby-theme-material-ui](https://www.gatsbyjs.org/packages/gatsby-theme-material-ui/)プラグインを使用すればよいです。
`src/gatsby-theme-material-ui-top-layout/theme.ts`を作成することで以下のようにテーマをカスタマイズできます。

```ts:src/gatsby-theme-material-ui-top-layout/theme.ts
import { createMuiTheme } from '@material-ui/core'
import { indigo } from '@material-ui/core/colors'

const theme = createMuiTheme({
  palette: {
    primary: { main: '#fbfb99' },
    secondary: { main: indigo.A400 },
  },
  props: {
    MuiLink: { color: 'secondary' },
    MuiTextField: { variant: 'outlined', color: 'secondary' },
  },
})

export default theme
```

## TypeScript対応

GatsbyはデフォルトでTypeScript対応していて、`tsconfig.json`等でカスタマイズすることもできます。
また、`gatsby-plugin-graphql-codegen`プラグインを使用することでGraphQLの取得データにも型補完が効いてかなり開発しやすくなります。
`gatsby-plugin-graphql-codegen`を使用する場合は以下の設定が個人的におすすめです。

[Gatsby+TypeScriptを快適にするためのgatsby-plugin-graphql-codegenの設定](https://qiita.com/dozensofdars/items/a5d5a68104cb6fea30cf)

## microCMSデータ取得

GatsbyとmicroCMSを連携するには先ほど述べた通り対応プラグイン[gatsby-source-microcms](https://www.gatsbyjs.org/packages/gatsby-source-microcms/)を利用できます。
microCMSはコンテンツごとに複数のAPIを作成できますが、その場合は以下のように`gatsby-config`でプラグインを複数回読み込むことでそれぞれデータ取得できます。

```ts:gatsby-config.ts
import { ITSConfigFn } from 'gatsby-plugin-ts-config'

const gatsbyConfig: ITSConfigFn<'config'> = ({ projectRoot }) => ({
  plugins: [
    // 中略
    {
      resolve: 'gatsby-source-microcms',
      options: {
        apiKey: process.env.GATSBY_MICROCMS_API_KEY, // APIキーは環境変数で管理するのが良いです
        serviceId: process.env.GATSBY_MICROCMS_ENDPOINT,
        endpoint: 'index', // APIのエンドポイント
        format: 'object', // オブジェクト形式の場合
      },
    },
    {
      resolve: 'gatsby-source-microcms',
      options: {
        apiKey: process.env.GATSBY_MICROCMS_API_KEY,
        serviceId: process.env.GATSBY_MICROCMS_ENDPOINT,
        endpoint: 'blogs', // APIのエンドポイント
        format: 'list', // リスト形式の場合
        readAll: true, // リスト形式の場合デフォルトでは最新10件のみ取得するので全件取得したい場合はこれをtrueにする
      },
    },
    // ...
  ]
})
export default gatsbyConfig
```

## microCMSのリッチエディタ(HTML)→JSX変換

microCMSで使用できるリッチエディタはHTMLをGUIエディタで作成できるのですが、APIでは生のHTML文字列として取得されます。
HTML文字列をReactでレンダリングするには

```tsx
const BlogPost: React.FC<{ body: string }> = ({ body }) => (
  <div dangerouslySetInnerHTML={{ __html: body }} />
)
```

のようにすればよいのですが、これだとHTMLそのままで出力されてしまうのでスタイルを適用するのがやや大変です。特にmaterial-uiのようなReactコンポーネントのライブラリの場合はこのままだと難しいです。
このHTMLにmaterial-uiのデザインを適用させるために、[rehype-react](https://github.com/rehypejs/rehype-react)というライブラリでHTML→JSXに変換しました。

まず、以下のように各タグ（以下は`h1`を想定）に対応するコンポーネントを作成します。

```tsx:Typography.tsx
import React from 'react'
import { Box, Typography, Divider } from '@material-ui/core'

type Props = { id?: string; className?: string }

export const Chapter: React.FC<Props> = ({ children, className, ...props }) => (
  <Box mt={2} mb={1} className={className}>
    <Typography {...props} variant="h4" component="h2">
      {children}
    </Typography>
    <Divider />
  </Box>
)
// ... 略
```

そして、`rehype-parse`でHTMLをパース・`rehype-react`でHTMLタグをJSXコンポーネントに変換する`render`関数を作成します。

```tsx:render.tsx
import React from 'react'
import unified from 'unified'
import parser from 'rehype-parse'
import rehypeReact from 'rehype-react'
import { Chapter } from '../components/Typography'

const processor = unified()
  .use(parser, { fragment: true })
  .use(rehypeReact, {
    createElement: React.createElement,
    components: {
      h1: Chapter,
      // ...以下タグとコンポーネントの対応を並べていく
    },
  })

// HTML -> JSX に変換する関数
export const render = (html: string) => processor.processSync(html).result
```

これをReactコンポーネント内で

```tsx
const BlogPost: React.FC<{ body: string }> = ({ body }) => (
  <div>{render(body)}</div>
)
```

といった感じで使用することで、microCMSから取得したHTMLにmaterial-uiのスタイルを適用してレンダリングできます。

## フォーム

[Netlify Forms](https://docs.netlify.com/forms/setup/)を利用すると静的サイトでもフォーム機能を利用することができます。
基本は`<form>`タグのアトリビュートに`netlify`か`data-netlify="true"`と記載するだけで使用できるのですが、Gatsbyのようにビルド時にHTMLを生成する場合はNetlifyがビルド時に上記アトリビュートを読み取れないので、以下のように`name="form-name", value=(formのnameの値)`と置いた`input`タグを挿入する必要があります。

```tsx
const Form: React.FC = () => (
  <form name="contact" method="POST" data-netlify="true">
    <input type="hidden" name="form-name" value="contact" /> {/* この行を追加 */}
    <p>
      <label>Your Name: <input type="text" name="name" /></label>   
    </p>
    <p>
      <label>Your Email: <input type="email" name="email" /></label>
    </p>
    <p>
      <label>Your Role: <select name="role[]" multiple>
        <option value="leader">Leader</option>
        <option value="follower">Follower</option>
      </select></label>
    </p>
    <p>
      <label>Message: <textarea name="message"></textarea></label>
    </p>
    <p>
      <button type="submit">Send</button>
    </p>
  </form>
)
```

また、ReCAPTCHAを利用する場合も`form`タグに`data-netlify-recaptcha="true"`を記述したうえで`<div data-netlify-recaptcha="true"></div>`という空の`div`タグを置けばNetlify側でReCAPTCHAを作成してくれるのですが、これもGatsbyでは読み取ってくれないので、`react-google-recaptcha`等のライブラリで実装する必要があります。

この辺は以下のようにラップしたフォームコンポーネントを作成しておくのが便利です。

```tsx:Form.tsx
import React from 'react'
import { makeStyles, Button } from '@material-ui/core'
import ReCAPTCHA from 'react-google-recaptcha'

const useStyles = makeStyles(theme => ({
  button: { marginTop: theme.spacing(2) },
}))

type Props = {
  name: string
  action: string
  className?: string
}

const Form: React.FC<Props> = ({ children, name, action, className }) => {
  const classes = useStyles()
  return (
    <form
      name={name}
      action={action}
      method="POST"
      data-netlify="true"
      data-netlify-recaptcha="true"
      className={className}
    >
      <input type="hidden" name="form-name" value={name} />
      {children}
      <ReCAPTCHA sitekey={process.env.GATSBY_SITE_RECAPTCHA_KEY ?? ''} />
      <Button type="submit" variant="outlined" className={classes.button}>
        送信
      </Button>
    </form>
  )
}

export default Form
```

## ビルド時・フォーム受信時等に通知する

Netlify Functionsにはビルドやフォーム受信等のイベントに反応して処理をトリガーする機能があります。
[Trigger serverless functions on events](https://docs.netlify.com/functions/trigger-on-events/#available-triggers)

特定のファイル名（デプロイ成功時なら`deploy-succeeded.js`等）でfunctionを作成することで、ビルド完了時にSlackに通知するといった連携機能を簡単に作成できます。

```js:funcions/deploy-succeeded.js
// 例：デプロイ成功時にSlackに投稿する
// Slack App の作成方法等は省略
require('dotenv').config()
const { WebClient } = require('@slack/web-api')

const slack = new WebClient(process.env.SLACK_TOKEN)

exports.handler = async (event, context) => {
  const { deploy_time } = JSON.parse(event.body).payload
  try {
    await slack.chat.postMessage({
      text: `ホームページが更新されました！ (ビルド時間 : ${deploy_time} 秒)`,
      channel: process.env.SLACK_CHANNEL_INFO,
    })
    return { statusCode: 200, body: 'ok' }
  } catch (error) {
    console.error(error)
    return { statusCode: 500, body: 'Internal server error' }
  }
}
```

# まとめ

ホームページをWordPressからGatsbyに移行して幸せになったお話と、ちょっとしたポイント集でした（まとまりがない…）。
具体的な構築方法は今回はあまり触れなかったのですが、Gatsbyは最近話題のフレームワークということもあって結構ドキュメント・記事が充実している印象で、調べながら割と簡単にホームページを作れました（例：[gatsby タグの記事一覧 - Qiita](https://qiita.com/tags/gatsby)）。
公式ドキュメントは今のところ英語だけですが、日本語訳も現在作成中（2020/08/05現在）のようなので楽しみです。
[gatsbyjs/gatsby-ja: Japanese translation of Gatsbyjs.org](https://github.com/gatsbyjs/gatsby-ja)