🙆

GWにWordpressからGatsbyへの移行調査した話

2021/05/07に公開

GWで時間があるうちにちょっと確認しておこうかなと思ってはまりました。
もったいないので忘れないうちに記事にしておこうと思います。

動機

もう5年前くらいから面倒を見させてもらっているサイトがあるのですが、そろそろリニューアルということでデザインを一新したいということになりました。
弊社はあまりシステム開発をともなわないスタティックなWebサイトは扱っていないのですが、デザイナーさんと組ませていただいて、今回のリニューアルも担当することになりました。

このサイトはいろいろと歴史的な紆余曲折がありまして、

  • 記事数は3000件くらい
  • 更新ユーザー(アクティブ)が20人くらい
  • やたら画像が重い(iPhoneで撮影してそのままアップロードしている)
  • 時々動画が上がっている(数MBを超える場合は連絡して削除していただく)
  • 毎日2〜3記事更新
  • 最長記事は6万字を超える。。。
  • PVはそこそこ。ピーク時3000PV/時くらい

というかなりヘビー級のWordpressサイトです。
何がヘビー級かって、エンドユーザーが自由きままに画像を貼り付けたり動画を貼り付けたりしているのでかなり重い。

さすがに重すぎる動画はYoutubeに上げてもらうようにお願いしているのですが、一部ユーザーはbmpとかばんばんアップロードしてきます。フリーダム!
png画像も8Mとかアップロードされてきます。制限するとせっかく活発に更新してくれているユーザーの手が止まってしまうので、あまりそこは厳しくしたくありません。

現在の構成

現在の構成は以下の通りです。

  • Amimoto Managed Hosting
  • Cloudfront CDN
  • EWWW Image Optimizerで画像最適化し、WebPで配信
  • 画像は全てS3にOffload(60G超)

一時サイトがあまりにも重くなってしまい、そこからいろいろと改善してきました。
しかしながらお客様持ち込みのテンプレートが機能多めの重い系なので、PageSpeedInsightは惨憺たる数字です。
最適化されたテンプレートに変更すればかなり改善されるであろうことは予想されたのですが、どうせならJAMStackにしてみたらどうかという話になりました。
PHPはあまり得意ではなく、Wordpressは専門外で永遠の初心者。Reactで書ければ嬉しいです。

Gatsby

Next.jsとGatsbyで迷ったのですが、Next.jsはWordpressとの連携事例があまりないようなので、Gatsbyを選択しました。

Gatsby(https://www.gatsbyjs.com/)とWordpressの接続は簡単です。

1. Gatsby Site作成

gatsby-source-wordpressのGetting Started通りに、新しいGatsby Siteを作ります。

gatsby new my-wordpress-gatsby-site https://github.com/gatsbyjs/gatsby-starter-wordpress-blog

こちらのStarterでは必要なプラグインの基本的な設定がされているので、WordpressのAPI Endpointを設定すればすぐに動きます。

2. Wordpress設定

テスト用にローカルのWordpress環境を作成します。とりあえずlocalを使いました。

https://localwp.com/

ダウンロードしてローカルで実行すれば、Wordpress一式を起動して、hostまで設定してくれます。
管理画面にアクセスして、以下のプラグインをインストールします。

3. エンドポイント設定

環境変数にlocalのWordpressのGraphQL Endpointを設定します。

export WPGRAPHQL_URL="https://yourwpsite.local/graphql"

4. 起動

gatsby develop

を実行するとGatsbyのサイトが立ち上がります。 http://localhost:8000 にアクセスすると、wordpressで作成した記事がstaticなHTMLに変換されて表示されます。

サイト移行検討

ということで、ここからが地獄です。

GatsbyのデータソースにHeadless Wordpressを設定するのは簡単でしたが、実際に移行したいサイトのデータでビルドできなければ利用できません。
また、現在のホスティングプランはHeadlessには高価すぎるため、ホスティングサービスの移行が必要です。

移行先の候補は以下の二つを考えました。

マネージドサービスは安くはないですが、自力でAWSなりのお世話をしている(時間的)リソースがないので、セキュリティまで面倒見てくれるのはありがたいです。

Shifter

現在のホスト先がAmimotoということで、移行先はまずShifterを考えました。
ShifterはWordpressのサイトをスタティックにコンパイルしてホストしてくれるサービスですが、HeadlessのWordpressホスティングプランもあります。

https://www.getshifter.io/pricing/

ちょっと高いですが、メディアストレージが無制限&画像最適化&CDNも無制限でwebPに変換して配信してくれる(Shifter Media CDN)ということで、移行候補では最有力に考えていました。

7日間のトライアル期間があるので早速登録して試用してみました。

サイトは1クリックで作成できます。作成されたサイトは既にHeadless用途に設定してあり、新たにプラグインを追加したり削除したりすることはできません。

とりあえず、データを仮移行して動作確認してみることにしました。

All-in-One WP Migrationが入っているので、データ移行はAll-in-One WP Migrationプラグインで行います。
移行元のサイトに同じプラグインを入れ、ドキュメントに従ってデータベースをエクスポートし、プラグイン経由でインポートしました。
ドキュメントによれば、この際移行できるのはデータベースの中身だけのようです。

https://support.getshifter.io/en/articles/3932865-importing-migration-data-to-shifter-headless

記事は無事に移行できたのですが、メディアファイルが問題でした。画像ファイルをWPの管理画面からアップロードすれば自動的にOffloadされるとのことでしたが、画像ファイルを移行しようとしたところ、All-in-One WP Migrationプラグインのファイルサイズが512MBに制限されています。
Shifter Media CDNのストレージに直接アップロードする方法があるのかと思ったのですが、サポート窓口に質問したところ、メディアファイルを一括でインポートする方法はないとのことでした。
そんなことってある?

また、現行のサイトはWP Offload Mediaで画像をオフロードしているため、画像URLをデータベース上で画像を配信しているCDNのURLに書き換えれば少なくとも過去画像を参照できるかと思ったのですが、あらかじめインストールされているSearch & Replaceプラグインで書き換えを試みるも、キャッシュが残っているのか、Shifter Media CDNが自動で書き換えているのか、元のURLがあちこちに残存している中途半端な状態になってしまいました。Redisのキャッシュを何度かクリアしても、REST APIのキャッシュをクリアしても直りません。
このあたりで完全に詰んだので、Shifterのトライアルを打ち切ることにしました。
完全に新規のサイトを制作するのでなければShifterの利用は難しいのかも知れません…

サイトは英語ですが、サポート窓口は日本語で回答してくれます。ただ、質問してから回答があるまで基本的に24時間以上掛かったため、途中で問い合わせを断念してしまいました。GW中ということもあったかと思いますが…

Headless Shifterに関してのドキュメントもあまりなく、ログを見る手段もないし調査のためにプラグインをインストールすることもできないので、基本的なこと以外をやろうとすると非常に厳しいです。

WPEngine

https://wpengine.com/

有名どころですが使ったことはなかったのでご新規様トライアルです。
といっても、トライアル期間のようなものはないので、いきなり課金することになります。60日間はRefundできるので、気軽に使ってくれよとのこと。

こちらもサイトは簡単なウィザードで作成できます。最近Headless Wordpressを推奨している?ようですが、特にHeadless専用という訳ではないので、普通のWordpressサイトです。

https://wpengine.com/support/headless/

バックアップから復元してステージングサイトを作ったり、過去のバックアップのバージョンから新しいサイトを作ったりできるのが結構便利でした。Prod, Staging, Developmentと三つ環境を持てるのも嬉しいです。

データ移行は専用のプラグインを使います。移行元のサイトにプラグインをインストールし、WPEngineのSFTPアカウントを設定して実行すると、バックグラウンドでちゃくちゃくとデータを移行してくれます。

https://wpengine.com/support/wp-engine-automatic-migration-powered-by-blogvault/

若干半信半疑だったのですが、ほぼ完璧にデータ移行してくれました。感謝。
カスタマイズしたテーマのレイアウトが崩れていたのですが、これはテーマ側に大いに問題があるので仕方なし。
一点、User Role Editorを使っているのですが、UserRoleの移行に失敗して管理者ログインできなくなってしまいました。多分UserRoleの設計が怪しく、若干データがよろしくなかったせいのような気がします。

サポートはチャットサポートですが、24-365で非常に早く応答してくれます。ログインできなくなったときもすぐに回答があり、調べて即その場でデータを修正してくれました。サポートのノリが陽気すぎて若干ビビることも。
英語が苦にならなければ非常にサポートは手厚い感じがしました。

LargeFS

WPEngineには、メディアファイルを自前のS3に保存できるLargeFSという仕組みがあります。S3のバケットを作ってpolicyを設定すれば、後はサポートにチャットで連絡するとさくっと設定してくれます。この辺、英語のコミュニケーションがおっくうな時には自分で設定したい…と思わなくもない。

https://wpengine.com/support/configuring-largefs-store-transfer-unlimited-data/

設定してもらうと、wp-content/uploadsにあるファイルは順次S3のバケットに移動されます。

今回の対象サイトでは、WP Offload MediaでMedia FileをOffloadしていたので、ここでもてこずりました。
単純にOffloadしているだけであれば、古いバケットから新しいバケットにオブジェクトをAWS cliでコピーすれば完了です。
しかし、WP Offload MediaでCDNのキャッシュ対策としてObject Versioningをオンにしていました。

WP Offload Media

通常、アップロードしたファイルは wp-content/uploads/2021/05/xxxx.jpegのようになりますが、このオプションがOnになっているとwp-content/uploads/2021/05/01161817/xxxx.jpegのようになります。LargeFSではこれに対応してくれないので、画像ファイルが表示できなくなります。

回避策

仕方がないので、WP Offload MediaとLargeFSを併用することにしました。

まずWPEngineでホスティングしているサイトの方にもWP Offload Mediaを入れます。WP Offload Mediaの参照するS3バケットをLargeFSが参照しているものに変更します。

プラグイン側でファイルを移動するか聞いてくるので、OKして全てのオブジェクトを新しいバケットに移動します。このとき、ストレージ側のパスがLargeFSと被らないように気をつけます。Moveと書いてあるのでどきっとしますが、コピーなので移行元のサイトには影響しません。

なお、原因はわからないですが、オブジェクトがすべて移動できなかったため、aws cliでコピーしました。

これで、過去のファイルはいままでと同じS3から取得できるようになりました。もっとなんとかする方法があったと思うんですが、他にも問題があったためこの辺で時間と気力が尽きました。

Headless化

WPEngineには専用のHeadless用プラグイン(WPEngine Headless)が用意されています。このプラグインを含め、以下の三つをインストールします。

WPEngineはWPEngine HeadlessとしてJAMStackをホストできる環境があるのですが、今回は利用しないことにしました。なので本当はWPEngine Headlessプラグインは不要かもしれない... 一応、これがあると通常のWordpressサイトへのアクセスをブロックしてくれるので入れています。

WPEngine Headless

WPEngine HeadlessではNext.js用のフレームワークを用意しています。

https://github.com/wpengine/headless-framework

npx create-next-app -e https://github.com/wpengine/headless-framework/tree/canary --example-path examples/getting-started --use-npm

としてnext appを作成すると、GatsbyのStarterと同じく、Headless Wordpressのエンドポイントを設定するだけでJAMStackなサイトが完成する & WPEngineでホスティングできるというコンセプトのようです。
いろいろ便利そうな機能が用意されていたのですが、試してみて利用を断念しました。

URLに記事タイトル(もしくはslug)を採用しているのですが、日本語を含むタイトルの記事を正常に取得してこられないようです。
ざっくり中身を見てみたところ、WPGraphQLのパラメータに渡すURIがBase64エンコードされたものになっているようです。
WPGraphQLでは、UTF-8の文字列を直接渡せば日本語uriでも記事を取得してくれるのですが、Base64エンコードされていると結果がnullを返してくるようです。

↓取得できない

query MyQuery {
  postBy(uri: "44K/44Kk44OI44Or") {
    id
    uri
    title
    excerpt
  }

↓取得できる

query MyQuery {
  postBy(uri: "タイトル") {
    id
    uri
    title
    excerpt
  }

Githubを見るとまだまだExperimentalのようなので、Gatsbyでいくことにしました。

Gatsby build

ここまで来たらゴールはあともう少し、と思ったのですが既にGWは後1日とかになっていました。
当初の予定では、動くことだけ確認したらプレビュー設定したりカテゴリの作り方を工夫したりCustom Post Type UIを試したりACFを試したりしようと思っていたのですが、動作確認すら終わらないままGWが終了しそうです。
というか以前もWPの移行に一週間以上悪戦苦闘したことを完全に忘れていました。自分は苦しいことはすぐ忘れる生き物のようです。

ちなみにゴールは全記事をWordpressから取得してGatsbyでビルドすることとしました。もっと色々遊びたかったんですが仕方ないですね。一応、とどこおりなく記事がビルドできることだけでも確認できれば、まずは選択肢として検討の俎上に上げられるのではないでしょうか。

画像が取得できない問題

ということで、ローカルで gatsby build してみました。いきなり3000件記事をビルドするのもどうかと思ったので、gatsby-config.jsにlimitを設定して、Wordpressから取ってくる記事数を制限します。

type: {
  __all: {
    limit: 20,
  },
},

で取ってくる記事数を20に制限できます。

gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: `gatsby-source-wordpress`,
      options: {
        // the only required plugin option for WordPress is the GraphQL url.
        url:
          process.env.WPGRAPHQL_URL ||
          `https://wpgatsbydemo.wpengine.com/graphql`,
        verbose: true,
        type: {
          __all: {
            limit: 20,
          },
        },
        develop: {
          hardCacheMediaFiles: true,
          hardCacheData: true,
        },
      },
    },
  ]
}

としてgatsby buildを実行したのですがエラー。この辺のログが残っていないのですが、どうも画像ファイルのパスが違うと怒られているようです。しかしWordpressの管理画面で投稿を確認しても、メディアライブラリを見ても画像は全て正常に表示されているので大分戸惑いました。

とりあえず、Plugin Optionを見て、画像をGatsbyで最適化せず、元のURLから配信するオプションがあるらしいので試してみました。

    html: {
      useGatsbyImage: false,
      createStaticFiles: false,
    },

しかし、ビルドを再トライしても相変わらず同じ画像でエラーが出ます。MediaItemの取得&画像ファイルのダウンロードも行われているようです。オプションの使い方はわからないまま、画像ファイルの403エラーの方を何とかすることにしました。
403エラーはS3が出しています。確認したところ存在しない画像を見に行っているようです。よく見ると、 IMGxxxx.jpg というファイルを参照すべき時に、IMGxxxx-scaled.jpgというファイルを見に行っています。
大分格闘したのですが、結局サイトで使用しているEWWW Image Optimizer(https://ewww.io/)を古いバージョンからアップグレードして使い続けている場合に、データベース内のURL文字列にゴミを残すことがあるとのこと。

EWWW Image Optimizerにデータベース最適化機能が用意されていたので最適化を実行し、履歴を削除して全ての画像を再最適化したところ、gatsby buildできるようになりました。

画像問題他にも

20記事ビルドできるようになったので、寝ている間にビルドしておこうと

type: {
  __all: {
    limit: null,
  },
},

としてgatsby buildしてほっておきました。朝起きると普通にエラー。まあそうですよね。
Wordpressから503エラーが返ってきているので、どうもサイトが過負荷になっているようです。
記事の同時取得数を減らします。

schema: {
  perPage: 30,
  timeout: 100000,
  requestConcurrency: 2, // currently set to 15
  previewRequestConcurrency: 2, // currently set to 5
},

一度に取得する記事数を30に減らし、タイムアウトも大幅に伸ばしました。同時リクエスト数も減らしました。
再度トライ。今度はしばらく順調だったのですが、途中でまたエラー。
Gatsbyでは255文字以上のファイル名は扱えないとのこと。探してみると超長いファイル名のPDFがアップロードされていたので、とりあえず名前を短くしました。
また、bmpファイルはgatsby-sharpで最適化できないというwarnが出ます。
それから、大きすぎるメディアファイルはダウンロードがタイムアウトするのでリサイズ。画像のダウンロードを回避する方法は結局わかりませんでした……

Building static HTML for pages

Wordpressの記事側に変なHTMLの記述があると、ビルド時にエラーになるようです。
おそらくテンプレート起因と思われるのですが、ところどころの画像に以下のような謎のstyleが設定されていました。

style="background-color: transparent; color: #333333; font-family: Georgia," times new roman","bitstream charter",times,serif; font-size: 16px; font-style: normal; font-variant: normal; font-weight: 400; height: 225px; letter-spacing: normal; max-width: 761.71px; orphans: 2; outline-color: #72777c; outline-style: solid; outline-width: 1px; text-align: left; text-decoration: none; text-indent: 0px; text-transform: none; -webkit-text-stroke-width: 0px; white-space: normal; word-spacing: 0px;"

後述のGatsby Cloudではビルド時にエラーが出たデータソースのパスを表示してくれるので、該当の投稿を表示し、テキスト表示してソースコードから怪しい記述を削除します。
保存して再ビルド、またエラーになった記事を探してWordpress側を直す、という作業の繰り返しで対応するしかなさそうです。
一応、一旦発見したエラー原因はSearch & Replaceで同じ記述がないか探し、一括削除します。上記の謎styleは489箇所もありました。

その他にも、元は絵文字らしき文字化けが見つかったり、謎の大量のspanタグが見つかったりするのを一つずつちまちま直していきます。

必ずしもITに詳しくないエンドユーザーが毎日活発に更新しているサイトなので、どうしても記事のHTMLが怪しくなるのは仕方がないのですが、SEO的にもあまりよくなさそうです。
また、記事中の記載が原因でビルドが失敗するとなると、今後ユーザーが更新作業を行うときに、ランダムに失敗すると嫌です。Gutenbelgとhttps://github.com/GatsbyWPGutenberg/gatsby-wordpress-gutenberg の組み合わせならこのあたりはうまくいくんでしょうか。

Gatsby側のビルド&ホスティング

そうこうしていると500記事まで正常にビルドできるようになったので、ビルドパイプラインにつないでみることにしました。

最初はVercelでホスト&ビルドしようと思ったのですが、遅い! 
Next.jsのプロジェクトで利用したときは非常にビルドが早かったのですが、Gatsbyだとかなり劇的に遅いです。
途中でタイムアウトした時点で断念。Gatsby Cloudでビルドすることにしました。

Gatsby CloudはGatsbyコミュニティのお墨付き(というか運営会社はGatsbyJS)なので、Gatsbyに最適化されています。
プランはbuildとhostに分かれているので、Gatsby CloudでビルドしてVercelでホストすることなども可能です。
もうだいぶめんどくさくなってきているので、とりあえずGatsby Cloudでそのままホストすることにしました。必要があれば他のホスティングに移動します。

Gatsby Cloudの何が嬉しいかといって、差分ビルドができることです。昔のGatsbyはデータソースの記事に変更があるたびに全ファイルをリビルドしていました。現在のGatsbyのバージョン(2.32.12)では、データソースの変更差分を確認して必要な部分だけ再ビルドしてくれる機能が実装されているそうなのですが、Gatsby Cloudはこれに対応しています。また、ビルドの実行自体もVercelより大分早いように感じます。

Githubにリポジトリを作り、手元のGatsby Siteをpushします。プライベートリポジトリでも問題ありません。
https://support.gatsbyjs.com/hc/en-us/sections/1500000069661-Getting-Started
公式のチュートリアルに従って作業すれば、迷うことはないと思います。

ちなみに一回のデプロイビルドの制限時間は4時間です。Vercelは45分間なので、やはり大きめのGatsbyサイトのビルドにはあまり向いていないですね。
調子に乗って全投稿3000件をビルドしようとしたらエラーで止まったので、とりあえず500件ビルドしてみて、全件ビルドできたら1000件にしてみて、というように順次ビルドしています。ちまちまエラーが出たら修正して、エラーが出たら修正して・・・ということを繰り返しています。

一度ビルドできてしまえば差分ビルドは早いです。新しく記事を追加したときのビルド時間は1分かからないですが、Previewでビルドを待っている時などはやはりちょっと待ち時間としては長いかなという感じがします。

プレビュー

プレビューの設定は非常に簡単です。
Gatsby CloudのSite SettingsにPreview WebhookとBuilds Webhookが表示されますので、Wordpress側のGatsbyJSプラグインにこのURLを設定するだけです。

Gatsby Cloud

Enable Gatsby Preview? にチェックを入れると、投稿画面のプレビューボタンで別タブが立ち上がり、Gatsby Cloudのビルドを待ちます。差分ビルドですが、通常のWordpressのプレビューのつもりでいるエンドユーザーには遅いと思われるかもしれません。

Builds Webhookも設定しておけば、記事を作成・更新すると自動的にビルドが走り、サイトが更新されます。複数記事の更新をかけるとその都度ビルドがキューに入るので、場合によっては更新反映にしばらくかかるかもしれません。

おわり

とりあえず、なんとか大きめのWordpressサイトをGatsbyでビルドできる見通しが立ったかと思います。
本当はGW中にAlgoliaを試してみたりCustom Post Typeを組み込んでみたりしたかったのですが、ここで時間切れです。Gatsbyで遊ぶつもりだったのに、ほぼWordpressと戯れていたら終わってしまいました。

細かい作り込みを要するWebサイトをJAMStackで作れるともろもろ嬉しいと思います。エンドユーザーにContentfulやPrismicを勧めるのは厳しいという場合でもWordpressなら何とかなるケースもあるかもしれません。
とはいえハードルは高いですが……

今後はクライアントにリスクとメリットを説明して、開発の方向性をどちらに舵を切っていくのか決定してもらうことになると思います。

中途半端な記事ですが、GWの供養とします。合掌。

Discussion