Shimabox Blog

~sit in the sun~

WordPressからCloudflare Pages + Honoに移行した話はてなブックマーク

はいさい。ついに長年の抱負であった「ブログの引っ越し」を達成しました。苦節7年。

2024年をふりかえる でも書いていたのですが、いい加減古びたWordPressから引っ越したいなぁとずっと思っていたんですよね。 で、今年に入り覚悟を決めてClaude Codeと結束し、ついに移行作業が完了しました。

なぜ移行したのか

理由はシンプルで、

  • WordPressがだるい
  • 何かしらモダンな構成で動かしてみたかった

これだけです。はい。

この時代、WordPressでブログを書いている人間はいるのか?という。 あと、記事を書くときにブラウザでWordPressの管理画面を開いて〜みたいなのもだるかったです。ZennみたいにMarkdownで書いたらサクッと反映される環境に憧れていたのもあります。

なぜ、Cloudflare Pages + Hono🔥なのか

移行先としてはいくつか選択肢がありました。

  • 静的サイトジェネレーター(Hugo, Astro, etc)
  • ヘッドレスCMS + フロントエンド
  • Cloudflare Pages + Hono

静的サイトジェネレーターでも良かったのですが、Cloudflare PagesやR2、KVを触ってみたかったというのが大きいです。あと、Honoが気になっていたというのもあります。

Honoは軽量でCloudflare Pagesとの相性が良く、JSXも使えます。ReactやVue.jsみたいなSPAにする必要もないし、シンプルにサーバーサイドでHTMLを返すだけで十分だったのでちょうど良かったです。

ℹ️ Note

このブログはブラウザからリクエストが来るたびにPages FunctionsがHTMLを生成して返す、昔ながらのMPA(Multi Page Application)方式です

Cloudflareは無料枠がけっこう太っ腹なのもポイントですね。

システム構成

システムアーキテクチャ

データフロー

  1. 記事の追加・更新
  • content/posts/YYYY-MM-DD-slug.md を作成・編集
  • GitHub にプッシュ
  • GitHub Actions が R2 に同期 → キャッシュを削除(プリウォーム)
  1. 記事の表示
  • ユーザーがページにアクセス
  • KV キャッシュをチェック(ヒットすれば即座に返却)
  • キャッシュミス時は R2 から取得 → Markdown パース → KV に保存
  1. キャッシュ戦略
  • TTL なし(無期限)で保持
  • 記事更新時に明示的に無効化
  • デプロイ直後にプリウォームで再生成

なぜTTLなし + 明示的無効化なのか

一般的なキャッシュ戦略ではTTL(Time To Live)を設定して、一定時間が経過したら自動的にキャッシュを破棄する方式がよく使われます。でも今回はあえてTTLなし(無期限)で保持し、記事更新時に明示的に無効化する方式を選びました。

TTLベースの問題点

TTLを例えば1時間に設定した場合、1時間ごとにキャッシュが切れます。そのタイミングでアクセスしたユーザーは、R2からの取得 → Markdownパース → KVへの保存という処理を待つことになり、体感で遅くなります(実際、めちゃくちゃ遅い)。 アクセス頻度が低い記事ほどこの問題は顕著で、久しぶりにアクセスされた記事が毎回キャッシュミスするという状況になりがちです。

明示的無効化 + プリウォームの利点

今回の方式では、キャッシュは明示的に消さない限り永続します。記事を更新したときだけGitHub Actionsで該当記事のキャッシュを削除し、すぐにプリウォーム(事前にアクセスしてキャッシュを再生成)します。

これにより、ユーザーがアクセスする時点では必ずキャッシュがある状態になっていて、初回アクセスの遅延がなくなります。

GitHub Actionsでやっていること

  1. R2に記事を同期
  2. 該当slugのKVキャッシュを削除
  3. そのURLにHTTPリクエストを送ってキャッシュを再生成(プリウォーム)

ブログのように「更新タイミングが明確」で「自分でコントロールできる」コンテンツには、この方式が相性良いと感じています。

技術スタック

項目 技術
フレームワーク Hono(JSX対応)
ホスティング Cloudflare Pages Functions
ストレージ Cloudflare R2(記事・画像)
キャッシュ Cloudflare KV(無期限、明示的無効化)
OGP画像生成 Satori + @resvg/resvg-js(ビルド時)
シンタックスハイライト highlight.js(CDN)
LiveReload livereload
Lint/Format Biome

セットアップ

# 依存関係のインストール
npm install

# 開発サーバー起動(LiveReload対応)
npm run dev
# → http://localhost:8787

コマンド

コマンド 説明
npm run dev 開発サーバー起動(LiveReload対応)
npm run deploy Cloudflare Pagesにデプロイ
npm run sync コンテンツをR2に同期
npm run sync -- slug 特定記事のみR2に同期
npm run generate-ogp OGP画像を生成
npm run generate-ogp -- slug --force 特定記事のOGPを上書き生成
npm run check Biomeでチェック
npm run check:fix Biomeでチェック&自動修正

ローカル開発体験

今回の移行で一番気に入っているのがこれです。

npm run dev

これだけで開発サーバーが立ち上がって、あとはVSCodeでMarkdownを書くだけ。ファイルを保存したらLiveReloadで即座にブラウザに反映されます。
裏側で livereload パッケージがファイルの変更を監視(livereload パッケージの watch() メソッドでファイル監視)していて、変更を検知するとWebSocket経由でブラウザに通知→自動リロードという流れです。

WordPressのときは、管理画面を開いて、エディタに書いて、プレビューボタンを押して…みたいな手順が必要だったのですが、今はZennと同じ感覚で記事が書けます。これは最高🔥

記事の追加

  1. content/posts/YYYY-MM-DD-slug.md を作成
  2. OGP画像を生成: npm run generate-ogp -- slug --force
  3. ローカル確認: npm run dev
  4. mainにpushすると自動デプロイ

frontmatter 形式

---
title: "記事タイトル"
slug: "my-new-post"
date: "2026-01-07"
categories: ["カテゴリ名"]
image: "/images/2026/01/thumbnail.jpg"  # オプション
---

固定ページ用 frontmatter

---
title: "About"
slug: "about"
date: "2010-04-19"
categories: []
fixedPage: true # 固定ページフラグ
---

fixedPage: true を設定すると

  • 目次が非表示
  • シェアボタンが非表示

となります。

機能一覧

コンテンツ機能

  • 記事一覧・詳細表示
  • カテゴリ別表示
  • ページネーション
  • 自動目次生成
  • RSSフィード (/feed/)

埋め込み対応

記事内のURLを自動的に埋め込みカードに変換:

  • X (Twitter): ツイートの埋め込み
  • YouTube: 動画プレイヤーの埋め込み
  • Gist: GitHub Gistの埋め込み

シェアボタン

記事詳細ページ下部に表示(固定ページ以外)

  • X (Twitter) でシェア
  • はてなブックマークに追加
  • CuraQ に保存
    • 要チェック

OGP

  • 自動生成(Satori + resvg)するようにして、静的ファイルとして保存して配信
  • /ogp/:slug.png でアクセス可能

URL構造

パス 説明
/ トップページ
/page/:page/ ページネーション
/category/:name/ カテゴリ別一覧
/:year/:month/:day/:slug/ 記事詳細(WordPress互換)
/about/ Aboutページ
/privacypolicy/ プライバシーポリシー
/feed/ RSSフィード
/ogp/:slug.png OGP画像
/images/* 画像配信

ディレクトリ構成

root/
├── .github/workflows/     # GitHub Actions(自動デプロイ)
├── content/
│   ├── posts/             # 記事(YYYY-MM-DD-slug.md)
│   ├── pages/             # 固定ページ(about.md, privacypolicy.md)
│   └── images/            # 画像、OGP画像(ogp/)
├── docs/                  # ドキュメント
├── fonts/                 # OGP生成用フォント(.gitignore)
├── functions/             # Pages Functions
│   └── [[path]].ts        # エントリポイント
├── public/                # 静的ファイル
│   ├── styles.css         # メインCSS
│   └── _routes.json       # 静的ファイルルーティング
├── scripts/               # ユーティリティスクリプト
│   ├── sync.ts            # R2同期
│   └── generate-ogp.ts    # OGP画像生成
├── src/
│   ├── index.tsx          # ルーティング
│   ├── markdown.ts        # Markdownパーサー(目次・埋め込み対応)
│   ├── repository.ts      # データ取得(R2/KV)
│   ├── rss.ts             # RSSフィード生成
│   ├── types.ts           # 型定義
│   └── views/             # JSXコンポーネント
│       ├── Layout.tsx     # 共通レイアウト
│       ├── PostList.tsx   # 記事一覧
│       ├── PostView.tsx   # 記事詳細
│       ├── Pagination.tsx # ページネーション
│       └── NotFound.tsx   # 404ページ
├── dev-server.tsx         # 開発サーバー(LiveReload対応)
├── biome.json
├── tsconfig.json
├── wrangler.toml
└── package.json

GitHub Actions

mainブランチへのpushで自動デプロイ。手動実行も可能:

deploy_type 説明
diff 変更されたファイルのみ同期(デフォルト)
full 全ファイルを同期
slug 指定したslugのみ同期
deploy 同期なしでデプロイのみ

移行作業

WordPressからの移行は、まぁまぁ大変でした。

記事データはwp-cliとかでエクスポートして、スクリプトでMarkdownに変換して、画像もダウンロードして…みたいな感じでゴリゴリやりました。 10年以上運用していたブログなので、記事数もそこそこあって、画像も大量にあって、なかなかしんどかったです。

あと、URL構造をWordPressと合わせるのも地味に大変でした。 /YYYY/MM/DD/slug/ という形式を維持しないと、過去にシェアされたリンクが全部死んでしまいますからね。

ただ、そのへんの変更スクリプトはClaude Codeがほぼほぼ自動生成してくれたので、かなり助かりました。AIすげー。🤖

また実際のブログシステムの動きも、Claude Codeがほぼほぼ実装してくれたので、僕は細かい調整やデザインの微調整に集中できました。AIすげー。🤖🤖

カスタムドメインの設定

最後に、blog.shimabox.net をCloudflare Pagesに向けました。

ネームサーバーはムームードメインのまま、CNAMEレコードを追加するだけで設定できました。 Cloudflare側でSSL証明書も自動発行してくれるので、特に何もしなくてもHTTPSで配信されます。便利すぎる。

かかった時間

Claude Codeに手伝ってもらいながら、だいたい3〜4日くらいでできました。 正直、自分一人でやっていたらもっと時間はかかっていたと思います。AIの力ってすげー。🤖🤖🤖

おわりに

というわけで、ブログをWordPressからCloudflare Pages + Honoに移行しました。

今後このブログで発信していきたいと思いますので、よろしくお願いします!

スポンサーリンク