WordPressブログのフロントエンドをGatsbyJS + Netlifyで構築する

WordPressブログのフロントエンドをGatsbyJS + Netlifyで構築する

今までWordPressでブログを運用してきたのだが、HTMLとPHPを絡めたテーマ開発が煩雑で表示速度も遅かったため、フロントエンドにGatsbyJS, Netlify, WP REST APIを導入することで改善した。

あまり時間がない中、ガガガッと移行したので忘れないように備忘録を残しておきたいと思う。

ブログのシステム構成

GatsbyJS + Netlify + WordPressのシステム構成

フロントエンドのソースコードはGithubに置いておき、masterへのプッシュをNetlifyが検知してGatsbyのソースコードを自動ビルドする。
このビルドでWordPressから記事データを引っ張ってきて静的ファイルを生成してくれる。
ビルド後はそのままNetlify上でサイトデータをホスティングという流れになる。

ブログの更新はこれまでと変わらずWordPress上ですべて管理していくことにした。
WordPressのView(テーマ)は触りたくないけど、CMSの機能はなんだかんだ柔軟だから別に変える必要ないよね、と考えた。移行も面倒くさい。
サイト表示ではViewを一切使わずにREST APIからデータを取得する、いわゆるヘッドレスCMSという位置付けで使う。

ヘッドレスCMSとは、Viewは利用せずにAPI経由でデータ出力するCMSのこと。

記事の更新時にもwebhookでNetlifyのビルドを実行するため特別な操作は不要。
ただしNetlifyでのビルドに多少時間がかかるので、記事の追加や変更の即時反映が難しいのがデメリットとしてある。(約300記事でおよそ2〜3分)

GatsbyJSとは

GatsbyJSはReact製の静的サイトジェネレータです。(以下「Gatsby」と表記する)

https://www.gatsbyjs.org/

ViewがReactで、Markdownや外部API(WordPress REST APIなど)からGraphQLで記事データを取得してHTMLとJSONを生成する仕組み。
アクセス時は静的HTMLを表示するものの、ページ遷移では生成されたJSONを読むので表示が非常に速い。注意点は、ソースコードの修正やブログの記事を更新した際は再ビルドが必要になることだ。しかし今回はNetlifyがwebhookで更新をキャッチするので心配ご無用。

ずっとWordPressのView開発が嫌で嫌でしょうがなくて、REST API直接叩いても良かったけどコストに見合わず速いわけではなかったので、パフォーマンスも伴ってお手軽にSPA化できる技術をずっと待ってた。

代替案としてNuxt.jsでも実現できるけど、Vue.jsそもそもちゃんと触ってない&イチから覚えるのも面倒だし、ブログのリニューアルにそこまでコストかけたくないということでGatsbyなのである。今はReact書くほうがはるかにラク。

Netlifyとは

2018年に人気が爆発したみんな大好き高機能ホスティングサービス。

https://www.netlify.com/

ソースコードのビルド〜デプロイ〜ホスティングまで全てが無料で使える。サイト数の制限はない。
Githubのリポジトリと紐づけてpushからの自動ビルド、そしてそのままNetlify上でホスティングできる。
さらにカスタムドメイン・SSL設定、SSR用のプリレンダリング、CDNで通信パフォーマンスが良いなどとにかく高機能なのである。

ベーシック認証などは有料にしないと使えないところもあるけど、個人ブログ程度なら無料枠で十分賄えるのでNetlifyを選択した。

あとWordPressが面倒な人にはNetlify CMSが使える。管理画面操作の裏でGitリポジトリへcommit・pushを自動化してCMSの様に振る舞ってくれる。

https://www.netlifycms.org/

WordPressからデータを取得する準備

GatsbyJS単体ではWordPressからデータを取ってこれないので、いろいろ解決できるgatsby-source-wordpressを使う。

gatsby-source-wordpress

特徴は以下。

  •  All entities are supported (posts, pages, tags, categories, media, types, users, statuses, taxonomies, site metadata, …)
  •  Any new entity should be pulled as long as the IDs are correct.
  •  ACF Entities (Advanced Custom Fields)
  •  Custom post types (any type you could have declared using WordPress’functions.php)

記事、固定ページ、タグ、カテゴリ、メディア、ユーザー、ステータス、タクソノミー、サイトのメタ情報などが取得できる。
カスタムフィールドプラグインのACFにも対応しているので、バックエンド連携としては必要十分だと思う。

そして、これらを含んだ公式テンプレートを使用することにした。

gatsby-starter-wordpress

DEMO: https://gatsby-starter-wordpress.netlify.com/

導入方法は以下。

// はじめにGatsbyを操作するための gatsby-cli をインストール
$ npm i -g gatsby-cli

// gatsbyコマンドが使えるようになる
$ gatsby -v
-> 2.4.17

// starterを使ってサイトデータの初期化
$ gatsby new PROJECT_NAME https://github.com/GatsbyCentral/gatsby-starter-wordpress

※PROJECT_NAMEは任意

できたディレクトリの中で yarn start を叩けば http://localhost:8000 が立ち上がる。簡単だ。

$ cd PROJECT_NAME
$ yarn start

WordPressとの紐付けは gatsby-config.js で行う。

plugins: [
  ...
  {
    resolve: 'gatsby-source-wordpress',
    options: {
      // The base url to your WP site.
      baseUrl: 'wp.yuhiisk.com', // WordPressのドメインを指定
      // WP.com sites set to true, WP.org set to false
      hostingWPCOM: false,
      // The protocol. This can be http or https.
      protocol: 'https',
      // Use 'Advanced Custom Fields' Wordpress plugin
      useACF: false,
      auth: {},
      // Set to true to debug endpoints on 'gatsby build'
      verboseOutput: false,
    },
  },

一旦は baseUrlprotocol を設定すればOK。
もう一度 yarn start し直すと自分のブログデータが入るはず。

あとはReactでガシガシViewを作っていく。
gatsby-starter-wordpressにはCSSフレームワークのbulmaも一緒に入ってて、今回はそれに乗っかった。

GraphQLを試す

GatsbyではGraphQLでデータを引っ張ってくる。

GraphQLはクエリ言語と呼ばれるWeb APIのための規格で、オブジェクト構文に似た記述でクエリを書くとAPI経由でデータを引ける。詳しくは割愛。

https://graphql.org/

yarn start してローカルサーバーが立ち上がるとGraphQLのGUIクライアントも一緒に立ち上がり、クエリの実行を試せるようになる。WordPressのデータを取得できているかはここで一度試すと開発が捗る。

http://localhost:8000/___graphql

GraphQL editor

query内では、以下でデータを取得する。

  • allWordpressPost で全投稿
  • allWodpressPage で全固定ページ
  • wordpressPost で個別投稿
  • wordpressPage で個別固定ページ

Gatsbyのコンポーネントにすでに書かれているので一度みれば雰囲気は掴める。他にもフィルターできたりいろいろ機能がある。

https://www.gatsbyjs.org/packages/gatsby-source-wordpress/#how-to-query

ビルド・デプロイ設定

そしてサイトのビルドとデプロイ設定を解説する。
大きく分けて次の3ステップとなる。

  1. Netlifyの設定
  2. WordPressの設定
  3. Githubへのプッシュ、もしくは記事の作成・更新によるデプロイ手順

Netlify上でのビルド & ホスティング

今回はGithubを使ったので、Githubアカウントがある前提で進める。

はじめにNetlifyでアカウントを作成。

https://www.netlify.com/

あとはGithubでフロントエンドソース用のリポジトリを作っておき、Netlifyでリポジトリを指定する。ビルド設定はそのままで良い。

GithubからのDeploy設定

Settings > Build & Deploy > Build Hooks でWebhook URLを生成しておく。

Deploy & BuildでBuildHook URLを生成

参考:Gatsby + Netlify + WordPressでHeadless CMS化してみた。

WordPressの更新をフックする

同一ドメインで置き換えたい場合は別のURLでWordPressにアクセスできるようにしておく。

なぜ複数のドメイン設定をしておくかというと、既存のWordPressに向いているドメインをNetlifyに設定するのだが、URLがひとつだけだとWordPressの管理画面にアクセスできなくなるし、後から設定では遅いから。新規で用意する場合は複数のドメイン設定は必要ない。

設定できたらWordPressにJAMstack Deploymentsをインストールする。

wp-jamstack-deployments

JAMstack Deploymentsは何かと言うと、記事や固定ページの更新タイミングで、Netlifyのビルドを実行できるプラグインだ。
このプラグインのおかげでWordPressから自動デプロイが可能になる。

プラグインをzipでDL/インストールして有効化すると、WordPress 設定 > Deployments から設定できる。
Webhook URLには先ほどNetlifyで生成したWebhook URLをPOSTで設定。
タイプは投稿と固定ページ、コンタクトフォーム(wpcf7_contact_form)にしておいた。
※見づらいけど一番下のSave Settingsを押さないと反映されない。

これで作成・更新・削除のタイミングでWebhookが飛ぶ。

そして gatsby-config.js などで指定していた、フロントエンド側のWordPressのURLを書き換えておく。

Githubへのプッシュ、もしくは記事の作成・更新によるデプロイ

あとはmasterへプッシュするかWordPressで記事を更新すれば、Netlify側で自動でビルドが走る。

Deployの進行中

ビルド・デプロイ完了後は自動的にNetlify上にホスティング、公開される。

システムの本番化

本番化はドメインを変更するだけ。

Netlifyのカスタムドメイン設定は以下の記事が参考になった。

参考:【Netlify】カスタムドメインを設定する

そしてこのままだとWordPress内のURLが古いので、phpmyadminから全て新しいURLへ置換しておく。

UPDATE `wp_posts` SET guid=REPLACE (guid,'https://blog.yuhiisk.com','https://wp.yuhiisk.com')
UPDATE `wp_posts` SET post_content=REPLACE (post_content,'https://blog.yuhiisk.com','https://wp.yuhiisk.com');
UPDATE `wp_options` SET option_value=REPLACE (option_value,'https://blog.yuhiisk.com','https://wp.yuhiisk.com');
UPDATE `wp_postmeta` SET meta_value=REPLACE (meta_value,'https://blog.yuhiisk.com','https://wp.yuhiisk.com');
UPDATE `wp_posts` SET post_title=REPLACE (post_title,'https://blog.yuhiisk.com','https://wp.yuhiisk.com');

この置換手順の場合、本番化〜置換までの間は記事内のリンクが繋がっていなかったり画像が表示されない時間が発生する。これが嫌な場合は別途新規のWordPressサーバーを用意しておくと良い。

最後にビルドして完了。
これからGatsby + Netlify + WordPressでのブログ新生活がスタートする。

ケーススタディ

TypeScriptを使いたい

TypeScript本体とGatsbyのプラグインを追加する。

$ yarn add gatsby-plugin-typescript typescript

gatsby-config.jsの pluginsgatsby-plugin-typescript を追加でOK。

plugins: [
  ...
  'gatsby-plugin-typescript’
]

src/templates内のファイルをtsにする場合、gatsby-node.js内で参照するファイル名も変更しておく。

GraphQLでエラーになる

GraphQL Error There was an error while compiling your site's GraphQL queries.
  Invariant Violation: GraphQLCompilerContext: Duplicate document named `IndexQuery`. GraphQL fragments and roots must have unique names.
    t: Duplicate document named `IndexQuery`. GraphQL fragments and roots must have unique names.

export const pageQuery = graphql`
  query IndexQuery($limit: Int!, $skip: Int!) {
    allWordpressPost(
      sort: { fields: date, order: DESC }
      limit: $limit
      skip: $skip
    ) {
      edges {
        node {
          ...PostListFields
        }
      }
    }
  }
`

IndexQuery と重複して名前をつけていた場合に発生した。プロジェクトを通してユニークな命名にする。

WordPressで設定したメニューを取得したい

WordPressにWP API Menusを入れるとREST APIからMenuが取れる。

"/wp-json/wp-api-menus/v2",
"/wp-json/wp-api-menus/v2/menu-locations",
"/wp-json/wp-api-menus/v2/menu-locations/(?P<location>[a-zA-Z0-9_-]+)",
"/wp-json/wp-api-menus/v2/menus",
"/wp-json/wp-api-menus/v2/menus/(?P<id>\\d+)",

GraphQLのクエリはこんな感じ。

query {  
  wordpressWpApiMenusMenusItems {
    name
    items {
      title
      url
    }
  }
}

WordPress Menus GraphQL Query

Twitterウィジェット表示したい

gatsby-plugin-twitterを入れる。

https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-twitter

$ yarn add gatsby-plugin-twitter

gatsby-config.jsの plugins に追加して完了。

plugins: [
  // ・・・
  "gatsby-plugin-twitter”, // 追加
}

あとはTwitterウィジェットを自動的に見つけて処理してくれる。

前後の記事を取得したい

Markdownだとfrontmatterで前後の記事を取れるようだが、WordPressだとGraphQLで工夫が必要。

そこでallWordpressPost からpreviousとnextを取る。

{
  allWordpressPost {
    edges {
      previous {
        title
        content
        slug
      }
      next {
        title
        content
        slug
      }
      node {
        ...
      }
    }
  }
}

上記からgatsby-node.js内のallWordpressPostを書き換える。

.then(() => {
  return graphql(`
    {
      allWordpressPost {
        edges {
          next {
            slug
            title
            date
          }
          previous {
            slug
            title
            date
          }
          node {
            id
            slug
            status
            date
          }
        }
      }
    }
  `)
})

取得したデータをcontextに渡す。

_.each(posts, ({ node: post, next, previous }) => {
  // Create the Gatsby page for this WordPress post
  createPage({
    path: `/archives/${post.slug}/`,
    component: postTemplate,
    context: {
      id: post.id,
      prev: previous,
      next
    },
  })
})

このcontextのデータは templates/post.js#Postprops.pageContext に渡ってくる。

Disqusを導入する

disqus-react を使用する。

$ yarn add disqus-react

render() {
  ...
  const disqusShortname = "yourdisqusshortname";
  const disqusConfig = {
    identifier: post.id,
    title: post.frontmatter.title,
  };

  return (
    <div>
      {/* 任意の場所で埋め込み */}
      <DiscussionEmbed shortname={disqusShortname} config={disqusConfig} />
    </div>
  );
}

参考:Add Disqus comments to a Gatsby blog

Webフォントを利用する

gatsby-plugin-web-font-loaderでWebフォントを設定する。

$ yarn add gatsby-plugin-web-font-loader

gatsby-config.jsにプラグイン設定を追加。

plugins: [
  ...
  {
    resolve: 'gatsby-plugin-web-font-loader',
    options: {
      google: {
        families: ['Droid Sans', 'Droid Serif']
      }
    }
  }
]

オプションの指定は以下URLを参照してほしい。

https://github.com/typekit/webfontloader

Contact Form 7で作成したフォームはどうすればいい?

結論から言うと、REST APIにエンドポイントがあるのでAjaxでフォーム送信できる。

まずエンドポイントの確認のためにHomebrewで jq を入れる。これは必須ではない。

$ brew install jq

参考:https://wp-kyoto.net/check-wp-api-custom-endpoint-by-jq

するとcurlで以下のように /wp-json のエンドポイントが一覧表示できる。URLは自分のWordPressサイトを指定する。

$ curl https://wp.yuhiisk.com/wp-json | jq .routes | jq keys

[
  "/",
  "/akismet/v1",
  "/akismet/v1/alert",
  "/akismet/v1/key",
  "/akismet/v1/settings",
  "/akismet/v1/stats",
  "/akismet/v1/stats/(?P<interval>[\\w+])",
  "/contact-form-7/v1",
  "/contact-form-7/v1/contact-forms",
  "/contact-form-7/v1/contact-forms/(?P<id>\\d+)",
  "/contact-form-7/v1/contact-forms/(?P<id>\\d+)/feedback",
  "/contact-form-7/v1/contact-forms/(?P<id>\\d+)/refill",
  ...

POST先は /wp-json/contact-form-7/v1/contact-forms/:id/feedback になる。

URLのパラメータid はContact Form 7で作成したフォームの記事IDで、フォーム内の input[name="_wpcf7"] に入っているのでそれを拝借する。

そしてリクエストヘッダーのContent-typeを application/json で、formdataをポーンと送るだけ。
レスポンスが正常なら完了UIを表示すれば良い。

フォーム側でrecaptcha設定してるとspam判定でレスポンスが返ってきて失敗するので一旦オフっておく。recaptcha対応は追記予定。

useEffectがエラーになる

gatsbyのreact-hot-loaderのバージョンが古いことが原因だったので、gatsby本体をアップデートした。

参考:Uncaught TypeError: Cannot read property ‘expirationTime’ of undefined

その後はGraphQLでフィルターを使うクエリを修正する必要があった。(gatsby v2.0.7 → v2.3.2)
filter内に elemMatch が必要。

// src/templates/category.js
// allWordpressPostのfilter->category->elemMatchを足す。

export const pageQuery = graphql`
  query CategoryPage($slug: String!) {
    site {
      siteMetadata {
        title
      }
    }
    allWordpressPost(filter: { categories: { elemMatch: { slug: { eq: $slug } } } }) {
      totalCount
      edges {
        node {
          ...PostListFields
        }
      }
    }
  }
`

Analyticsの設定どうする?

gatsby-plugin-google-analyticsを入れる。

$ yarn add gatsby-plugin-google-analytics

plugins: [
  ...
  {
    resolve: `gatsby-plugin-google-analytics`,
    options: {
      // replace "UA-XXXXXXXXX-X" with your own Tracking ID
      trackingId: "UA-XXXXXXXXX-X",
    },
  },
],

参考:Adding analytics

Adsenseの設定どうする?

プラグインは無いので react-adsense を使って設定する。

まずhtml.jsをキャッシュディレクトリからsrcにコピーする。

// プロジェクトルートで

$ cp .cache/default-html.js src/html.js

src/html.jsを用意すると、html全体をレンダリングするRoot Componentを上書きできる。

Customizing html.js

このファイルの中でadsenseの読み込みを追加。

<head>
  <meta charSet="utf-8" />
  <meta httpEquiv="x-ua-compatible" content="ie=edge" />
  <meta
    name="viewport"
    content="width=device-width, initial-scale=1, shrink-to-fit=no"
  />
  <script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js" /> // 追加
  <script dangerouslySetInnerHTML={{ __html: "(adsbygoogle = window.adsbygoogle || []).push({ google_ad_client: \"ca-pub-****************\", enable_page_level_ads: true });" }} /> // 追加
  {props.headComponents}
</head>

あとは適当に埋め込み用のAdsenseコンポーネントを作る。

import * as React from 'react';
import AdSense from 'react-adsense';

type Props = {
  client: string;
  slot: string;
  format: string;
};

const Adsense = ({ client, slot, format }: Props) => (
  <div>
    <AdSense.Google
      client={client}
      slot={slot}
      format={format}
    />
  </div>
);

export default Adsense;

これを任意の場所で使えば良い。

<Adsense client="ca-pub-****************" slot="**********" format="auto" />

参考:GatsbyJSにGoogle AdSenseを導入する

ページのlocationを取得したい

Props.locationに入っているので、詳しくはgatsby-linkを参照してほしい。

Gatsby Link

SSRのカスタマイズどうする?

基本は不要だけど、プロジェクトルートに gatsby-ssr.js を用意して設定可能。

Gatsby Server Rendering APIs

OGP対応

基本は react-helmet でmetaタグを設定する。

設定方法は公式で紹介されている。
Adding an SEO component

自分は以下のソースを参考にした。
https://github.com/jlengstorf/marisamorby.com/tree/master/src/components/SEO

react-helmetのAPIは目を通しておくと良い。
react-helmet

あとメタ情報を取得するためのStatic Query実行の際は useStaticQuery カスタムフックが使える。(Gatsby v2.1.0~, react v16.8~, react-dom v16.8~ が条件)
Querying data in components with the useStaticQuery hook

window参照できない問題

ローカルでは問題ないけど、Netlifyでビルドするとwindowオブジェクトがないと怒られる。

解決策は、Reactの componentDidMountuseEffect の中で参照する。

Netlifyのビルドエラー

GraphQLでデータ取得に失敗した場合のエラー。

GraphQLError: Cannot query field "allWordpressPage" on type "Query". Did you mean "allSitePage" or "allWordpressAcfOptions"?

REST APIにアクセス出来ない場合に発生する。自分の場合は国外からのREST APIアクセスを制限していたためGraphQLが弾かれていた。

一応URLが間違っていないかも確認しておいた方が良い。

Build時にstyled-componentsのスタイルが当たらない

Gatsby プラグインを入れることで回避した。
gatsby-plugin-styled-components

plugins: [
  ...
  {
    resolve: `gatsby-plugin-styled-components`,
    options: {
      // Add any options here
    },
  },
],

記事の日付が1日前になってしまう

日付の整形処理は実行環境のTimezoneに依存する。
NetlifyのNode.js環境はUTCになるので、特に指定しなければ-9時間される。

moment-timezonedate-fns-timezone を入れる。

$ yarn add moment-timezone

or

$ yarn add date-fns-timezone

// moment-timezone

const moment = require('moment-timezone');

const timeZone = 'Asia/Tokyo';
const date = moment(post.date).tz(timeZone).format('YYYY/MM/DD');

// date-fns-timezone
const { formatToTimeZone } = require('date-fns-timezone');

const timeZone = 'Asia/Tokyo';
const date = formatToTimeZone(post.date, 'YYYY/MM/DD', { timeZone });

記事タイトルが文字化けしたら

HTML Entitiesをパースする。

const unescape = (str: string): string => {
  return str.replace(/&#(\d+);/g, (match, dec) => {
    return String.fromCharCode(dec);
  });
};

unescape(post.title);

カテゴリの親子階層が認識されない

小カテゴリを廃止して1階層のみのフラット構造にする。

bulmaのスタイルが当たらないんだけど

利用していないCSSはgatsby-plugin-purgecssでビルドの際に除去されるが、一方でWordPressの記事データ内のHTMLクラス名までは解析されない。
そのためダミーのコンポーネントを用意して、その中で当てたいクラス名をつけたコンポーネントを定義しておく。

const Features = () => (
  <div>
    <textarea className="textarea" name="" id="" cols="30" rows="10"></textarea>
    <button className="button is-link">submit</button>
  </div>
);

export default Features;

Gistが埋め込めない&WordPressサイトのメタ情報欲しい

後日追記予定です。

以上。

五十川 洋平(Yohei Isokawa)

五十川 洋平(Yohei Isokawa)

フロントエンドエンジニア/面白法人カヤックなどのWeb制作会社に勤務したのち、故郷の新潟に戻り独立。JSフレームワークAngularやFirebase、Google Cloud Platformを使ったWebアプリ開発が得意。Udemyでプログラミング解説の講師や、ドローンを使った映像制作も行っています。

プロフィール