はじめに
技術部では、事業部横断的な仕事としてコーポレートサイトの運用も行っています。このたびWordPress on EC2で運用されてきた弊社のWebメディア(Q by Livesense)を、Hugo on Clouflare Pagesに移行しました。
弊社のWordPress運用はやや特殊で、エンジニアがサーバーにSSHしてテーマの更新作業を行なっており、運用の手間が大きくなっていました。 このため、静的サイトジェネレータへの移行と開発体験の向上はエンジニアの悲願でした。
今回はbefore/afterの技術構成と、移行にあたって工夫したこと、大変だったことととその成果をご紹介します。
技術構成(before)と課題
移行前の構成ではWordPressで運用しており、EC2/RDSとWordPress本体の管理は外部の会社に委託していました。
EC2とRDSを2つのAvailability Zoneで冗長化し、メインとサブのサーバー間でコンテンツを同期する構成です。
WordPressで管理しているコンテンツ(記事とテーマ)の更新は自分たちで行なっており、テーマはGitHubで管理して、変更を加える際はEC2インスタンスにSSHして温かみのある手作業で更新してきました。
この構成には以下のような課題がありました。
- WordPressの管理画面から更新できるものなのか、そうでないのかの判別が難しい
- コンテンツを更新したいとき、WordPressの管理画面から更新できるものなのかテーマを更新すべきなのかわからないため、毎度知見のあるエンジニアがソースコードと画面を見ながら確認することになる
- WordPressのアカウントを持っていない人は気軽に変更できない
- GitHubのようにPull Requestを出して変更差分のレビューを必須とすることができないので、いきなり本番環境に誰かが変更を加えてしまうリスクがあり、アカウントの作成を制限していた
- デプロイが自動化できない
- WordPress本体のコンテンツ管理機能やプラグインがあるため、コード管理もCI/CDも難しい
- WordPress本体やプラグイン、PHPのバージョンアップとサーバーのメンテナンスが面倒
これらの課題は、WordPressの運用を行ったことがあるエンジニアであれば覚えのあるものではないでしょうか。
今回の移行では、これらの運用上の課題を解決して開発と運用の生産性を向上させるとともに、静的サイト化することでセキュリティリスクを減らし、サイトのパフォーマンスを向上させることを目標としました。
技術構成(after)と選定の理由
移行後の構成では静的サイトジェネレータ(Hugo)を利用し、サイト全体を静的なものにしました。またCloudflare社が提供するJamstackプラットフォーム(Cloudflare Pages)を利用することでインフラの管理が不要になりました。
静的サイトジェネレータにはさまざまなものがあります。今回Hugoを採用した理由は、インフラグループメンバーにHugoの運用経験がある人が複数いたためと、Goへの馴染みがあったためです。
また、ホスティングサービスとしてCloudflare Pagesを採用した理由は、GitHubとの連携機能が強くcommitごとにプレビュー環境を生成する機能が用意されており、それでいてほぼ無料枠内で賄えるほどにコストが安かったためです。プレビュー環境についてはアクセス制限も必要になりますが、Cloudflare Zero Trustを利用し、Emailのドメインによる認証など柔軟にポリシーを設定できる点も魅力でした。
このような強いGitHub連携機能により、Cloudflare Pagesではリポジトリを連携する簡単な設定を行うだけで「Pull Requestのマージをトリガーとしてデプロイを行う」といった自動化が実現できます。
改善したこと
パフォーマンスの向上
静的サイト化することで、動的にHTMLを生成するWordPressに比べて表示速度が上がりました。さらにCSSやJSのminifyやキャッシュバスターの実装、Cloudfrale PagesでのブラウザキャッシュのTTLの設定、アクセシビリティ周りの実装など、全体的なパフォーマンスの向上を目指し改善を加えました。
また前後でLighthouseのスコアを比較すると、パフォーマンスが向上したことが一目瞭然です。
移行前はトップページにローディングを入れていましたが、安定的に高速になったことでローディング画面の実装も不要となり、撤去しています。
デリバリー速度の向上
Jamstackに移行したことで、念願のCI/CDの導入が叶いました。
インフラグループでは標準的なCI/CDツールとしてGitHub Actionsを利用しています。今回の移行でもこれを採用し、JSやマークダウンのLintを行っています。
またCloudflare PagesへのデプロイについてはGitHub Actionsすら使う必要がなく、標準でPull Requestのマージをトリガーに自動化する機能がついています。
さらにHugoはローカルでの開発が非常に楽です。WordPressをローカルで起動する際は、Dockerなどを使ってコンテナを用意し、さらにDBにデータを入れる作業が必要でした。これに対しHugoでは、CLIを使って簡単にサーバーが起動でき、またコンテンツのマークダウンもリポジトリ内に内包するためデータの挿入も不要です。ローカルでの開発がやりやすかったことにより、HTML/CSSの大幅なリファクタリングが可能となり、全体的な可読性とパフォーマンスを向上させることができました。
Cloudflare PagesではPull Requestごとにプレビュー環境が生成されます。このプレビューがあることで、GitHubで差分を見ながら、そして画面を確認しながらデザインなどの細かい調整を行うことができるようになりました。
またHugoでは環境ごとに設定ファイルを分ける機能が用意されています。
これを利用し、プレビュー環境ではドラフト記事もビルドするようにフラグを設定して、下書きのレビューをできるようにしました。
これと同様に、CSSやJSのminifyもローカル環境では無効、プレビュー環境=ステージング環境や本番環境では有効、というように分けてフラグを設定し、開発しやすくしています。
WordPress環境下では、他の人の作業に影響を与えてしまわないように気を使う必要がありました。それがプレビューやCI/CDの導入で、それぞれが自由に開発を進められるようになり、リリースフローが劇的に向上しました。また、社内のGitHubアカウントを持つ人なら誰でもコントリビュートできるようになったので、ちょっとした変更を加えるのも気軽にできるようになりました。
セキュリティ面でのリスク低下
WordPressはPHPで動的にコンテンツを生成しており、また管理画面のURLの特定も容易であるため、外部からさまざまな攻撃にさらされます。静的サイト化したことによって、PHPやWordPressの脆弱性から解放されたのは大きな成果です。
大変だったこと
移行にあたってたくさんの課題に直面しました。
特に初期の検証ではわからなかった細かな問題が実装をしていくうちにたくさん出てきたので、大変だったものを紹介します。
記事のマークダウン変換
Hugoのコンテンツはマークダウン書式で書くのが一般的です。マークダウン書式はとても簡素に記載できて便利ですよね。ところが、Q by Livesenseは書式のこだわりが多いので、マークダウン書式そのままでは対応できなかったりします。
段落分けと改行の区別
Q by Livesenseでは文章内で段落と改行を明確に分けています。
Q by Livesenseは長文を書くメディアであるため、編集者に「長文が書きにくい」という印象を与えたくなかったので、マークダウンのフォーマットの比較検討をしていました。
マークダウンを扱うエディタでは、通常の改行をレンダリング時も改行として扱ってくれるものが多々あります。しかしHugoのマークダウンのレンダリングでは、標準設定では改行は以下のように扱われます。
- 空行なしの改行は無視
- 空行(≒改行2回)を入れると段落(
<p></p>
タグ)を分ける - 行末のバックスラッシュや半角スペース2つは改行(
<br>
)に変換
レンダリング設定を変更することで動作を変えることもできますが、どうしても望まない半角スペースが混入したり、後述の字下げの問題が起きたりしてしまいました。半角スペースが入るだけならわずかな誤差のようですが、マークダウンのレンダリングルールを標準から逸脱させてしまうと、後述するさまざまな書式に影響が出て、問題が積み重なってしまうことが懸念でした。
そのため、標準通りに扱うことにしました。改行したいときは行の末尾にバックスラッシュを入れます。バックスラッシュが <br>
に変換されるイメージです。空行で区切ると段落( <p></p>
)が分かれます。
字下げ
Q by Livesenseでは行ごとに1文字分の字下げを行っています。
WordPressのときは、全角空白文字を入れていました。マークダウンにもその書式を引き継ぐのか?書きにくくないか?もっと良い実装はないか?頭を悩ませました。
CSS3には行頭を字下げするためのtext-indent
プロパティがあります。text-indent:1em
とでもすれば、1文字分の字下げが実現できるわけです。ただし、このパラメータは<p>
タグや<div>
タグの単位で動作します。1段落内に<br>
区切りで複数行が入っている場合、それぞれの行頭には字下げが行われません。
そのため、改行後も字下げするeach-line
パラメータが定義されていますが、現状では実装されているブラウザがないようです。
字下げのためにすべての改行を<p>
で表現する実装方法も検討しましたが、その場合は前述の通り、標準的なマークダウンから外れていくことが気になりました。
さらに困ったことに、Q by Livesenseでは一部の文章表現では字下げをしないことがあります。これをどう区別するかが、マークダウンからのレンダリングとCSSでは表現不能でした。
結果として、色々試して、WordPressと同じく、全角の空白文字を意識的に入力して字下げする書式を選びました。格好悪いのですが、字下げが制御できるということで、柔軟さを優先した形です。
ちなみに、このような比較検討が全置換によって一度に簡単にできること、それをプレビューで確認できることは、Hugoのメリットでした。(WordPressだと、1記事1記事、管理画面を開いて直す必要があります)
書式の追加
その他、標準的なマークダウンだけでは表現できない以下のようなものをHugoのショートコードで実装しました。
- 引用(色んなパターンがある・・・)
- 補足
- さまざまな箇条書き
- 囲み
- ルビ
- 傍点
今後もこのような書式の追加はあり得るのですが、HugoのショートコードではGoTemplateによる凝ったロジックが組めるため、実装を比較的容易にできていることで助かっています。
Lintが必要になった
上記のように改行をバックスラッシュや2つの半角スペースで表すような厳密な書式としたことで、改行を忘れるケースが散見されるようになりました。改行は取り扱い方がエディタによって違うので、マークダウン対応エディタに慣れている人ほどミスしやすい状態です。「完全な空行」と、「スペースだけが入った行」を目視で区別することも難しくて厄介でした。
特にWordPressから記事を一度にインポートした初回移行時にはミスが多く、目視で全てのミスを発見するのが大変でした。
そこで用意したのがPythonによる自作のLinterです。GitHub Actionsと組み合わせ、記事の更新時にはLINTが行われて、改行忘れなどのミスしやすい箇所に警告を出すようにしました。
このPythonコードはシンプルなので、今後も機能拡張が容易です。自作のショートコードの利用時のミスも、LINTで気付けるようになることでしょう。
マークダウン化したことで発生するようになったミスではありますが、自動テストによってミスに気がつくことができるという、モダンなブログになったと言えます。
記事ごとのOGP画像周りの実装
FacebookやX等にURLをシェアする際に利用されるOpen Graph Protocol(OGP)では、サムネイル画像のサイズをメタデータに持たせることを要求しているものがあります。画像サイズを手動で入力するのはとても面倒なので、自動計算させようとしました。また、OGP用の画像とはいえ、キャッシュもしっかりと効かせられるようにしたかったため、キャッシュバスターを実装しようとしていました。
Hugoでは以下のようなコードを書くことで実装できます。(色々省略してます)
{{ $image := resources.Get $ogpImagePath }} {{ $imageURL := "" }} {{ with $image }} {{ $imageURL = printf "%s%s?m=%s" ($baseURL | strings.TrimSuffix "/") .RelPermalink ($image.Permalink | md5) }} {{ end }}
このOGP画像、配置することを忘れたり、名前を間違えたりすることが想像されました。OGPの情報はHEAD内に記載される内容なので、プレビューでも気が付きにくい問題です。
そのため、プレビュー環境(ドラフト記事をビルドする条件)ではOGP画像が見つからないときにエラーを記事内に出すように実装しました。
{{ if and site.BuildDrafts }} {{ $ogpImagePath := printf "images/ogp/%s.png" (.Date | time.Format "2006-01-02") }} {{ $ogpImage := resources.Get $ogpImagePath }} {{ if not $ogpImage }} <div> OGP画像ファイルが見つかりません:<br>/assets/{{ $ogpImagePath }} </div> {{ end }} {{ end }}
これによって、記事執筆者がOGP画像を設定し忘れることを防止するようにしています。
URL変更に伴うリダイレクト設定
WordPressとHugoでは標準で出力されるURLが違います。Q by LivesenseはカスタムURLの機能を使わずWordPressで出力されるURLをそのまま使っていたため、移行前はパスが下記のようになっていました。
- /2023/07/12/1787.html
※ 実際には複数の記事が同日に公開されることはありませんでした。
これに対しHugoの標準のURL形式は下記のような形です。
- /2023/07/12/
末尾に .html
をつける形式は、Hugoの設定のuglyURLsというフラグを使うことで実現できます。しかしファイル名を拡張子に含む形式(uglyURL)はその名の通りHugo的には推奨されていないため、移行に当たってはHugoの標準のURL形式を使いつつリダイレクトで解決することにしました。
当初はHugoのリダイレクト機能を使おうと考えており、個別の記事のマークダウンにaliasesを設定しようとしていました。しかしHugoのaliasesはstatus code 301でリダイレクトするのではなくmeta refreshを使ってリダイレクトを実現する機能でした。meta refreshはGoogle非推奨のリダイレクト方法で、SEOの評価が引き継がれないなどの悪影響があるため、ホスティングサービス側のリダイレクトの機能を使うことにしました。
Cloudflare Pagesのredirectの機能を使うと、_redirects
というファイルを置き、正規表現を使ってリダイレクトパターンを書くだけで、自動的にリダイレクトしてくれます。この機能を使ってリダイレクトを実現しました。
標準の検索機能がない
Hugoは静的サイトジェネレータで、かつ実装でもJavaScriptを使っていないので、標準の検索機能がありません。「記事の中から特定の検索ワードが含まれるものを一覧表示する」といったよくある機能も、自前で実装する必要があります。
Hugoに標準の検索機能がないことはコミュニティでも何度か話題になっており、公式にはいくつかの実装手法が紹介されています。
ここで紹介されている解決方法を参考に、検索機能を自前で実装しました。具体的には紹介されているこちらのgistを元に、jQueryを使わずにVanilla JSで実装しました。
また、日本語分かち書きをより正確に扱うために、こちらのHugoのテーマ開発者の方の記事を参考に修正を加えました。
おわりに
Hugo + Cloudflare Pagesに移行したおかげで、WordPress on EC2の運用に伴う多くの課題が解決されました。また日々の運用でも、Jamstackならではのパフォーマンスの良さや開発の自由度を実感しています。
Q by Livesenseはシンプルなブログメディアなので、移行最初期に試験的に実装を行った際は1週間もかからずにそれなりのものができたため、移行はすぐに完了できそうに思われました。しかし本格的に実装を行なってみると、意外にハマりどころがあったり、既存のバグの発見・改修も含む細かな作業が大量に発生したりして、なかなかに大変なプロジェクトになりました。当初の想定よりは時間がかかりましたが、非常に開発しやすくなったのは間違いありません。また、静的サイト化によってセキュリティ周りの心配がほぼなくなったのも嬉しいところです。
Q by Livesenseのような日本語縦書きメディアの裏側がこのようなモダンな技術構成になっているというのには、ある種の面白さがあります。なお、日本語分ち書きの実装についてはこちらの記事に詳しく書いてあるので、興味のある方はご覧ください。
Webメディアのインフラというのは目立たないものですが、今回の移行を機会に裏側についてご紹介しました。