はてなブログをCloudflare Workersへ移行した1日

CloudflareClaude CodeExperience

パーマリンク

このブログ「君のてのひらから」は、これまではてなブログの独自ドメイン機能でyourpalm.jubenoum.comを運用してきましたが、昨日Cloudflare Workersへ移行しました。サイト自体は静的サイトジェネレータのAstroで構築し、公式の@astrojs/cloudflareアダプタでCloudflare Workers向けにビルドしています。記事本文の執筆・同期フローは引き続きはてなブログ側で行い、はてな公式のhatenablog-workflows(GitHub Actions)で記事データをリポジトリに同期しつつ、実際に読者の目に触れるサイトはAstroでビルドしてCloudflareでホストする、という構成です。画像はR2に配置します。

移行作業自体は数日前から少しずつ進めていたのですが、実際に本番ドメインを切り替えたのは昨日です。デザインの方針やドメイン移行の判断は私が決め、実装・調査はClaude Codeに任せる、という分担で、朝から晩まで作業を続ける1日になりました。備忘録として、やったことと得られた学びをまとめておきます。

トップページをカードグリッドに

移行に着手する前段階として、まずトップページの記事一覧を見直すことにしました。最初は記事タイトルと日付だけを並べたシンプルなテキストリストで、正直、味気ない見た目でした。方向性を相談しつつ、Claude Codeに実装を任せたところ、次のような形になりました。

  • サムネイル画像・カテゴリバッジ・抜粋を含むカードグリッドに変更
  • サムネイルのない記事(実は大半)は、カテゴリ名からハッシュ関数で決定的に生成したグラデーションのプレースホルダーで補う

「記事本文中の<img>タグ(Amazonアフィリエイトの1x1トラッキングピクセルなど)を安易にサムネイル候補にすると事故る」というのは、コーパスを調査してもらって初めて知った点でした。

SEO対策一式

トップページが形になったところで、SEO面の基礎固めに進みました。

  • meta descriptionOGP・Twitter Card一式
  • 移転記事の擬似リダイレクト(JSのカウントダウン)を、Cloudflareの_redirectsによる本物のHTTP 301に置き換え
  • sitemap.xmlrobots.txt
  • 記事ページへのBlogPosting構造化データ(JSON-LD)

このうち一番効いたと感じたのは301リダイレクトの修正です。今まで移転先へは「3秒後に自動で転送されます」というJSページを経由していたのですが、これは検索エンジンからすると単なる200 OKのページにしか見えず、旧URLに貯まった評価が新URLへほとんど引き継がれない状態でした。静的な_redirectsファイルに切り替えるだけで、ちゃんとした301として振る舞うようになります。

RSS・カテゴリページ・404ページなど

「他に改善点はありますか?」と聞いたところ、いくつか具体的な提案をもらったので、優先度の高いものから対応しました。

  • RSSフィード(/rss.xml): はてなブログ時代は自動配信されていたので採用
  • カテゴリ別アーカイブページ(/category/{名前}/): 実装の途中でカテゴリ名の表記ゆれ(Obsidian/obsidianなど6組)がClaude Codeによって発見され、あわせて統合
  • カスタム404ページ:ベースの状態だとないのかと気がついて実装を追加
  • 記事ページのパーマリンク表示:シェアするときに便利だと思っているので追加
  • 自サイトへのリンクカードの改善(実際の記事タイトル+アイキャッチ画像で表示)

表記ゆれは手作業でタグ付けしていると、こういうことが普通に起きるんだなと実感しました。

いよいよ本番ドメインの切り替え

一通りサイトの体裁が整ったところで、コストを確認してから本番ドメインの切り替えに着手しました。このブログ程度(1か月で1000アクセスくらいのペース)Cloudflare Workers・R2・KVはいずれも無料枠に大きく余裕があります(※無料枠の範囲は執筆時点・2026年8月のものです。Cloudflareの料金体系は変更されることがあるので、実際に検討する際は最新の公式情報を確認してください)。

切り替え自体は、Cloudflareの「カスタムドメイン」機能(wrangler.jsoncroutesを1行追加してデプロイするだけ)で完結します。 画像にみられる通り、1行に対して、コメントが非常に長くなっておりますが…。

対応として、はてな側の管理画面で独自ドメイン設定を解除しました。これにより、今後の下書きプレビューはjubenoum.hatenablog.jp側で生成されるようになります。実はこの記事自体、その動作確認を兼ねています(このドラフトのプレビューURLもjubenoum.hatenablog.jpドメインで発行されているはずです)。

OGP画像が表示されない、を追いかける

ドメイン切り替え後、スマホから実際にSNSで記事のシェアを試みたところ、プレビューカードに画像が出ないことに気づきました。ここからが今日一番手こずったところです。

Claude Codeにサーバー側を一つずつ確認してもらいました。

  1. og:imageタグの中身は正しい絶対URLになっている
  2. 画像自体は問題なくアクセス可能(サイズも1200×670で十分)
  3. Twitterbotを偽装したUAでアクセスしても、Cloudflareにブロックされず200が返る
  4. DNSも(Google DNS・Cloudflare DNS・通常の名前解決すべて)正しく伝播している

ここまで確認してもまだ直らず、私が何度かスマホで再テストを繰り返しながら、最終的に2つの原因にたどり着きました。

1つ目はCloudflareの「Bot Fight Mode」 ドメイン上のボットトラフィックを検出してチャレンジする機能なのですが、これが実際のSNSクローラー(Twitterbot等)を弾いてしまっていたようです。Claude CodeがUAを偽装したテストでは素通りしてしまうため、DNS・画像の到達性・og:imageタグの中身と、サーバー側を一通り確認し終えるまでこの設定には気づけませんでした。このブログのようにログインフォームも決済もない静的サイトでは、Bot Fight Modeが守ってくれるリスクよりも、SEOやSNS共有のために「良いボット」を通したいメリットの方が大きいと判断し、オフのままにしています。

同じ症状に当たった方への注意点として、ログインフォームや会員機能を持つサイトではBot Fight Modeを無条件にオフにするのはおすすめしません。あくまで「守るものが少ない静的サイトだから」という前提での判断です。

2つ目は、R2から画像を配信するWorkerのレスポンスにContent-Lengthが設定されていなかったこと。 ストリームをそのまま返していたため、チャンク転送になっていました。R2オブジェクトの既知サイズから明示的にContent-Lengthを設定するよう、Claude Codeに直してもらいました。

さらにこの過程で、サムネイルのない記事(全体の約8割)向けに「サイトロゴ+記事タイトルを合成したフォールバック画像」を@napi-rs/canvasで自動生成する仕組みも作ってもらいました。日本語タイトルをビルド時に描画する必要があるため、GitHub Actions側にCJKフォント(fonts-noto-cjk)をインストールするステップも追加しています。

振り返り

今日1日で、デザイン・SEO・インフラ移行・トラブルシューティングと、幅広い作業をやりました。個々の作業自体はそれほど難しくないのですが、こうして並べてみると「静的サイトを1つ本番運用する」ということの裏には、地味だけど無視できない作業がたくさんあるのだと改めて感じます。

特にOGP画像のトラブルシューティングは、サーバー側の確認だけでは埒が明かず、Cloudflareのダッシュボード設定(Bot Fight Mode)まで辿り着く必要がありました。「サーバーは正常なのにクライアント側で再現しない」系の不具合は、原因が自分の管理下にない設定に潜んでいることがある、というのは頭の片隅に置いておこうと思います。

Claude Codeにブランチを切ってPRを作らせ、レビューしてマージする、というワークフローを1日通して回し続けましたが、今日だけで20件近いPRが積み上がりました。1つ1つは小さな変更でも、こまめにレビューできる単位に分けてもらえるのはやはり安心感がありますね。自分はこんなにきちんとPR書けないな…と思いながらレビューしていました。

この状態でしばらく運用してみようと思います。