Featured image of post 8年運用したWordPressブログをHugo(静的サイト)に移行した話【EC2卒業】

8年運用したWordPressブログをHugo(静的サイト)に移行した話【EC2卒業】

WordPress + EC2 で運用していたブログを Hugo + S3 の静的サイトに移行した経緯、手順、ハマったところをまとめました。

5431文字

はじめに

このブログは2017年の開設からずっと、AWS の EC2 上に立てた WordPress で運用してきました。ただ、ここ数年は更新頻度が落ちる一方で、サーバー代とメンテナンスの手間だけがかかり続ける状態に。

思い切って WordPress をやめて、Hugo で作る静的サイトに移行しました。この記事はその移行後、最初に書く記事です。先に「移行して何が変わったか」を書いて、そのあとで、なぜ移行したのか、どう移したのか、どこでハマったのかを、これから同じことをする人向けにまとめます。

移行前と移行後のトップページはこんな感じです。

移行前(WordPress)のトップページ

移行後(Hugo + Stack テーマ)のトップページ

先に結論:移行して何が変わったか

  • 記事を書く流れが「エディタで Markdown を書いて push」だけになった。サーバーを起動する儀式が無い
  • サーバーの更新・監視・バックアップが不要になった
  • ページの表示が軽くなった(PHP も DB も無い、ただの HTML)
  • 月額コストは 20 ドル強から、EC2 停止後は数百円程度になる見込み
  • 旧記事の URL と画像はそのまま。被リンクも検索流入も失っていない
  • サイト内検索が使えるようになった

最後の検索は、静的サイトにしてむしろ増えた機能です。WordPress の検索はサーバー上の PHP が DB に問い合わせて結果ページを作る仕組みなので、StaticPress で HTML に書き出したサイトでは動きません。「入力に応じてその場で作るページ」は書き出しようがないからです。なので旧サイトには検索フォーム自体を置いていませんでした。

Hugo では、ビルド時に全記事のタイトル・本文・URL を 1 つの JSON に書き出し、検索ページがそれをブラウザで読み込んで JavaScript で絞り込みます。サーバー側の処理はなく、S3 に置いた静的ファイルだけで完結します。記事 429 本で索引は 2.5MB ほど。記事を足せば再ビルドで勝手に更新されます。

サイト内検索。索引の JSON をブラウザで絞り込むだけなのでサーバーは不要

代わりに、コメント機能や管理画面からの画像アップロードのような「動的なもの」は無くなりました。このブログではどちらも使っていなかったので、失ったものはほぼありません。

ここからは、なぜそうしたのか、どうやったのかの話です。

移行を決めた理由

お金

EC2(t2.micro)、Elastic IP、EBS、Route53 と積み上がって、月に20ドル強。しかも記事を書いていない月も同じだけかかります。静的サイトなら S3 と CloudFront だけで、このブログのアクセス規模だと月に数百円で収まる見込みです。

手間とセキュリティ

WordPress 本体、PHP、プラグイン、OS のアップデート。放置すると脆弱性が溜まり、更新すると何かが壊れる。書きたいときにサーバーの面倒から始まるのが、更新が遠のく原因になっていました。静的サイトには攻撃される「実行中のもの」がないので、この心配がまるごと消えます。

書くたびに EC2 を起動して、終わったら止める

サーバー代を抑えるために、書かないときは EC2 を停止していました。すると記事を書くたびに、AWS のコンソールにログインして EC2 を起動し、立ち上がるのを待ってから管理画面を開く、という手順から始まります。書き終わったら StaticPress で静的化して、EC2 を止めるのを忘れないようにする。忘れると、次に気づくまで課金が続きます。

「思いついたときにサッと書く」とは程遠い儀式で、これが地味に一番効いていました。静的サイトには起動も停止もありません。

書き方を Markdown と git にしたかった

記事は Windows のデスクトップでも Mac のノートでも書きます。WordPress では記事の実体がサーバー上の DB にあるので、どの PC からでも「まずサーバーを起動して管理画面を開く」ことになり、上の儀式が毎回ついて回ります。

そこで、ブログのデータをひとつのリポジトリに持ち、どの PC からでも同じものを開いて続きを書ける形にしたかった。記事は Markdown のテキストファイル、管理は git。clone すれば手元に全記事がそろい、履歴も残る。エディタで書いて git push するだけ、という流れにしたかったのが最後の一押しです。

移行後の構成

項目移行前移行後
生成WordPress(PHP + MySQL)Hugo(静的サイトジェネレータ)
テーマ独自カスタマイズStack(Hugo テーマ)
配信EC2 → 静的化プラグイン(StaticPress) → S3 / CloudFrontS3 / CloudFront(そのまま流用)
記事管理画面のエディタMarkdown + git(GitHub のプライベートリポジトリ)
画像サーバーにアップロード旧画像は S3 に据え置き、新規は記事フォルダに同梱して git 管理

構成図で並べるとこうなります。移行前は「書く場所」と「配信する場所」の間に EC2 上の WordPress と静的化プラグインの StaticPress が挟まっていて、しかも書く前に EC2 を起動し、書き終わったら止める、という手順がセットでした。移行後は EC2 が丸ごと消えて、手元で書いた Markdown を GitHub に push すると、ビルドと S3 への同期が自動で走る形になります(自動化は準備中)。

配信面(S3 + CloudFront)は WordPress 時代から StaticPress で使っていたので、移行後も URL 構造をそのまま維持できました。/2024/03/01/12064/ のような旧 URL も、画像の /wp-content/uploads/... もそのままです。被リンクや検索流入を失わないために、ここは最初から必須条件にしていました。

リポジトリの中身はこうなっています。テーマ本体は触らず、サイト側のフォルダに設定と少しの上書きだけを置く構成です。

リポジトリの構成(抜粋)
blogenist/
  config/_default/
    hugo.toml        基本設定・URL の形式・コードハイライト
    params.toml      テーマ(Stack)の表示設定
    menu.toml        サイドバーのメニューと SNS リンク
  content/posts/     記事(Markdown)。旧記事は <記事ID>.md、新記事は <番号>/index.md
  data/              商品リスト、動画リスト(TOML)
  layouts/           テーマの上書きとショートコード(必要最小限)
  assets/scss/custom.scss   配色などの調整
  static/            ads.txt、favicon など
  themes/stack/      テーマ本体(無改変)

移行の手順

1. WordPress からエクスポート

管理画面の「ツール → エクスポート」で WXR(XML)を出力。記事本文、カテゴリ、タグ、アイキャッチの紐づけ、添付ファイルの情報がすべて入っています。記事512本で 15MB ほどでした。

2. WXR を Markdown に変換

Python で変換スクリプトを書きました。標準の XML パーサーだと WordPress の出力に含まれる不正な文字で落ちるので、正規表現で <item> を切り出す方式にしています。骨格はこれだけです。

wxr2hugo.py(骨格)
import re, html

raw = open("export.xml", encoding="utf-8").read()
items = re.findall(r"<item>(.*?)</item>", raw, re.DOTALL)

def cdata(block, tag):
    m = re.search(rf"<{tag}>\s*<!\[CDATA\[(.*?)\]\]>\s*</{tag}>", block, re.DOTALL)
    return m.group(1) if m else ""

for block in items:
    if cdata(block, "wp:post_type") != "post":
        continue
    if cdata(block, "wp:status") != "publish":   # 下書き・非公開・ゴミ箱は持ち込まない
        continue
    post_id = cdata(block, "wp:post_id")
    title   = html.unescape(cdata(block, "title"))
    body    = cdata(block, "content:encoded")
    # ... 本文の整形、front matter の組み立て、content/posts/<post_id>.md へ保存

変換後の記事は、こういう front matter を持った Markdown になります。

content/posts/12064.md(先頭)
---
title: "記事タイトル"
date: 2024-03-01T12:00:00+09:00
url: "/2024/03/01/12064/"            # 旧 URL をそのまま固定
image: "/wp-content/uploads/2024/02/eyecatch.jpg"   # アイキャッチは S3 の旧画像
categories:
  - "ガジェット"
tags:
  - "GoPro"
words: 2161                          # 文字数・所要時間は変換時に前計算(理由は後述)
minutes: 5
wpPostId: 12064
---

変換でやっていることは次のとおりです。

  • url: に旧 URL(/YYYY/MM/DD/<記事ID>/)を書いて、パーマリンクを固定
  • _thumbnail_id → 添付ファイルのパス、と辿ってアイキャッチを復元
  • WordPress 時代の独自ショートコード(Amazon 商品リンク、関連記事リンク)を Hugo のショートコードに置き換え
  • 旧テーマ専用の装飾(太字クラスやマーカー)は、Markdown 標準の <strong><mark> に変換
  • ハイライトプラグインのコードブロックを Markdown のコードブロックに変換(これも後述)

最後の2つは地味に大事で、旧テーマの CSS クラスを引きずると、テーマを変えるたびに「旧記事だけ崩れる」ことになります。移行時に標準の書き方へ寄せておくと、後が楽です。

3. 画像は動かさない

画像は 4,000 枚以上・700MB あります。これを git に入れるのは現実的ではないので、S3 にあるものをそのまま使うことにしました。パスも変えないので、記事本文の画像リンクは一切書き換えていません。ローカルで画像込みの表示を確認したいときだけ S3 から同じフォルダにコピーし、git からは除外しています。

.gitignore(抜粋)
**/public/
**/resources/_gen/
**/static/wp-content/uploads/    # 旧画像は S3 が正。ローカルにコピーしても git には入れない

新しく追加する画像だけ、記事フォルダに同梱して git 管理にしています。

4. ローカルで確認

hugo server で手元に立ち上げ、トップ・記事・カテゴリ・タグ・検索を一通り確認。

ローカルプレビュー
cd blogenist
hugo server -D --renderToMemory     # -D で下書きも表示、--renderToMemory でディスクに書き出さない

全記事のビルドは 6秒です。WordPress の管理画面を開くより早い。

ビルド結果
                  │ JA
─────────────────┼──────
 Pages            │  607
 Paginator pages  │  425
 Static files     │ 4195
 Aliases          │  120

Built in 6376 ms

ハマったところ

目次が出ない

WordPress の本文は HTML なので、見出しも <h2> のままです。Hugo の目次(.TableOfContents)は Markdown の見出しからしか作られないため、変換時に HTML の見出しタグを Markdown の見出しに変換して解決しました。

変換前(WordPress の本文)
<h2>移行を決めた理由</h2>
<h3>お金</h3>
変換後
## 移行を決めた理由

### お金

記事同士を参照するとビルドが止まる

関連記事カードで「リンク先記事の文字数」を出そうとして、テンプレートからリンク先の本文集計を参照したところ、ビルドが数分経っても終わらなくなりました。記事 A が記事 B の本文を要求し、B が A を要求し……と互いに待ち合う形になるためです。

ダメな例(リンク先の本文を集計しに行く)
{{ $target := site.GetPage .Params.url }}
<span>{{ $target.WordCount }}文字 / 約{{ $target.ReadingTime }}</span>

対策は、文字数や読了時間を変換時に前計算して front matter に持たせ、カードはそれだけを読むようにすること。リンク先の front matter(タイトル・画像・日付・文字数)だけで完結させれば、一瞬でビルドが終わります。

こうした(front matter の値だけ読む)
{{ $target := site.GetPage .Params.url }}
<span>{{ $target.Params.words }}文字 / 約{{ $target.Params.minutes }}</span>

コードブロックの色分けが消えた

WordPress ではハイライト用のプラグインを使っていたので、本文にはプラグイン独自の <pre> が 1,400 個ほど残っていました。Hugo はこれをただの HTML として素通しするので、色分けもコピー用ボタンも無い状態に。

変換前の表示。ただの黒い箱で、色分けもコピー用ボタンもファイル名も無い

変換前(プラグインの形式)
<pre class="lang:sh decode:true" title="command">brew install hugo</pre>

これを Markdown のコードブロックに変換したら、Hugo 標準のハイライトがそのまま効きました。ファイル名やコマンド名(title)は、Hugo の render hook でブロックの上に帯として出しています。

変換後(Markdown)
```bash {title="command"}
brew install hugo
```

コードブロックの表示(色分け・コピー用ボタン・タイトル帯)

OGP の説明文に \n が混ざる

SNS でシェアしたときの説明文に、改行が \n という文字のまま出ていました。WordPress 由来の本文に Windows 形式の改行や連続空白が含まれていたのが原因で、説明文を作るテンプレートで空白類をまとめて整形するようにして解決。

layouts/_partials/data/description.html
{{- $d := .Summary | plainify | htmlUnescape -}}
{{- $d = replaceRE "[\\s\\x{3000}]+" " " $d | strings.TrimSpace -}}
{{- return $d -}}

移行直後は、実際に投稿したときのカード表示まで確認しておいたほうがよさそうです。

非公開にした記事が公開されたままだった

これは Hugo というより静的化の落とし穴。WordPress で「非公開」や「削除」にした記事が、StaticPress で S3 に書き出したページとしては残っていて、URL を知っていれば普通に見られる状態になっていました。静的化は「追加・更新」はしてくれても「削除」までは面倒を見てくれなかったわけです。

移行後は、ビルド結果を S3 に同期するときに --delete を付けて、ビルドに含まれないページは S3 からも消えるようにしました。記事を下げたいときは Markdown を消せば終わりです。

S3 への同期(旧画像のフォルダだけは消さない)
aws s3 sync public/ s3://<バケット名>/ --delete --exclude "wp-content/uploads/*"

「ちょっとカスタマイズ」の誘惑

テーマに無い機能を自前の CSS や JS で足し始めると、テーマを更新するたびに直す羽目になります。今回は「テーマ標準の設定で済むならそれを使う。自前で書くなら最小限」を方針にして、途中でテーマも乗り換えました(最初は別のテーマで進めていましたが、左サイドバーや目次の追従など、欲しいものが標準で揃っていた Stack に切り替え)。

それでも、記事の前に出す商品カードや動画カードのように、自前で組んだ部品はいくつかあります。何を自前で足したかは README に一覧で残して、テーマ更新のときに見比べられるようにしています。

記事の前に出るおすすめアイテムとおすすめ動画(自前の部品)

これから

  • GitHub Actions で push → ビルド → S3 同期 → CloudFront キャッシュ無効化まで自動化
  • 旧 EC2 と Elastic IP を削除して、コスト削減を確定させる
  • 商品紹介カードや関連記事カードなど、WordPress 時代に便利だった部品を引き続き整備

移行作業そのものは、休日の作業で2日ほど。エクスポートさえ取ってしまえば、あとは手元で何度でもやり直せるので、思っていたより気楽でした。WordPress の維持に疲れている人には、静的サイトへの移行はかなりおすすめです。

Use: Hugo
Theme: Stack(by Jimmy)