今までWordPressでブログを運用してきたのだが、HTMLとPHPを絡めたテーマ開発が煩雑で表示速度も遅かったため、フロントエンドにGatsbyJS, Netlify, WP REST APIを導入することで改善した。
あまり時間がない中、ガガガッと移行したので忘れないように備忘録を残しておきたいと思う。
ブログのシステム構成
フロントエンドのソースコードは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,
},
}, 一旦は baseUrl
と protocol
を設定すれば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
query内では、以下でデータを取得する。
allWordpressPost
で全投稿allWodpressPage
で全固定ページwordpressPost
で個別投稿wordpressPage
で個別固定ページGatsbyのコンポーネントにすでに書かれているので一度みれば雰囲気は掴める。他にもフィルターできたりいろいろ機能がある。
https://www.gatsbyjs.org/packages/gatsby-source-wordpress/#how-to-query
ビルド・デプロイ設定 そしてサイトのビルドとデプロイ設定を解説する。 大きく分けて次の3ステップとなる。
Netlifyの設定 WordPressの設定 Githubへのプッシュ、もしくは記事の作成・更新によるデプロイ手順 Netlify上でのビルド & ホスティング 今回はGithubを使ったので、Githubアカウントがある前提で進める。
はじめにNetlifyでアカウントを作成。
https://www.netlify.com/
あとはGithubでフロントエンドソース用のリポジトリを作っておき、Netlifyでリポジトリを指定する。ビルド設定はそのままで良い。
Settings > Build & Deploy > Build Hooks でWebhook 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側で自動でビルドが走る。
ビルド・デプロイ完了後は自動的に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の plugins
に gatsby-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
}
}
}
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#Post
の props.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の componentDidMount
か useEffect
の中で参照する。
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-timezone
か date-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サイトのメタ情報欲しい 後日追記予定です。
以上。