Joifup Blog

NotionブログをSSG化してCloud RunからCloudflare Pagesに移行しました

2026-01-30

この記事では、NotionをCMSにしたブログのインフラをGoogle CloudからCloudflare Pagesに移行した経緯や、移行に伴う課題、実施した具体的な手順、移行後の成果について詳しく解説します。

Next.jsNotionCloudflareTerraformGitHub Actions

はじめに

この記事では、NotionをCMSにしたブログのインフラをGoogle Cloud(Cloud Run)からCloudflare Pagesに移行した話を書いていきます。

前編(Webhook × GCS編)では、Notion APIの画像URL有効期限問題に対してWebhook × GCSで永続化する方法を紹介しました。動くようにはなったんですが、運用してみるといろいろ課題が出てきて、結局アーキテクチャごと見直すことにしました。

個人ブログのインフラ移行を考えている人や、Notion CMSの画像問題に困っている人向けです。

前編方式の課題

前編で構築したWebhook × GCS方式、動くことは動くんですが、運用していくうちに3つの課題が見えてきました。

Webhookの信頼性が微妙

前編記事でも触れましたが、Notionのwebhookは発火条件が多すぎます。ちょっとした編集でも飛んできてしまうし、配信失敗や順序保証もありません。画像同期の基盤としては正直少し不安定でした。

コストが個人ブログにしては高い

Cloud Run + GCS + Cloud CDN + Cloud Buildで月額約2,800円。特にCloud CDNを使うために必要なロードバランサーの固定費(転送ルール1つで約$18/月)が大きくて、個人ブログのトラフィック量に対して明らかにオーバーでした。

構成が複雑すぎる

Notionで画像を更新 → Webhookが発火 → Cloud Runで受信 → Notionから画像取得 → GCSにアップロード → Cloud CDN経由で配信。と、パイプラインが長めです。

なぜSSG × Cloudflare Pagesなのか

そもそもの問題は「Notionの画像URLに有効期限がある」ことです。Webhook方式ではリアルタイムに画像を同期していましたが、冷静に考えると個人ブログにリアルタイム性は必要ありません。

ビルド時にNotionから画像をダウンロードして静的ファイルとして配信すれば、Webhookも GCSも不要になります。SSG(Static Site Generation)なら画像もHTMLも全部まとめて静的ファイルにできるので、ホスティングもシンプルです。

ホスティング先としてCloudflare Pagesを選んだのは、無料プランでも無制限のリクエスト・帯域幅が使えるからです。個人ブログならこれで十分すぎます。

構成の変化はこんな感じです:

Before: Notion → Cloud Run(ISR) → GCS + Webhook + Cloud CDN
After:  Notion → GitHub Actions(ビルド時画像DL) → Cloudflare Pages

だいぶシンプルな構成となりました。

やったこと

1. Next.jsのSSG化

まずNext.jsの設定を変更しました。

// next.config.ts
const nextConfig: NextConfig = {
  output: "export",
  images: {
    unoptimized: true,
  },
};

output: "export" でSSGモードに切り替えて、images: { unoptimized: true } でNext.jsの画像最適化を無効化しています。SSGではサーバーサイドの画像最適化が使えないので、この設定が必要です。

あわせて、ISR用の revalidate 設定を削除し、dynamicParams = false を追加して、ビルド時に生成されないパスへのアクセスは404を返すようにしました。

2. ビルド時画像ダウンロードスクリプト

SSGではビルド時に画像を手元に持っている必要があるので、Notionから画像をダウンロードするスクリプトを作りました。

仕組みとしては、Notion APIで全公開記事を取得し、カバー画像とコンテンツ内の画像をダウンロードして public/images/posts/{slug}/ に保存します。画像はsharpでwebp形式に変換してファイルサイズを削減しています。

ポイントは .image-manifest.json による増分ダウンロードです。一度ダウンロードした画像はマニフェストに記録しておいて、次回ビルド時にはスキップします。記事が増えても毎回全画像をダウンロードする必要がないので、ビルド時間が無駄に伸びません。

3. TerraformでCloudflare Pages + DNS構築

インフラ側は既存のTerraform構造に、Cloudflare用のモジュールを追加しました。

  • cloudflare_pages モジュール: Cloudflare Pagesプロジェクトの作成
  • cloudflare_dns モジュール: Zone作成 + カスタムドメインのCNAME設定

GitHub連携はせず、GitHub Actionsからデプロイする構成にしています。DNS移管もこのタイミングでCloudflareに移しました。

4. GitHub Actionsでビルド & デプロイ

.github/workflows/deploy.yml を作成して、手動トリガー(workflow_dispatch)でビルドからデプロイまで自動化しました。

ポイントは画像のキャッシュです。actions/cachepublic/images/posts/.image-manifest.json をキャッシュしておくことで、2回目以降のビルドでは新規・更新画像だけダウンロードすればよくなります。

デプロイは wrangler-action でCloudflare Pagesに nextjs/out ディレクトリ(SSGの出力先)をそのまま上げるだけです。

5. GCPリソースの全削除

Cloudflare Pagesでの動作確認とDNS切り替えが完了した後、不要になったGCPリソースを全部削除しました。

  • Cloud Run(joifup-blog-nextjs
  • GCS(joifup-blog-assets
  • Artifact Registry(nextjs-repo
  • Cloud Build Trigger
  • Service Accounts

Terraform側はGCPモジュールの呼び出しを削除し、関連ファイル(cloudbuild.yamldocker/docker-compose.yml)もリポジトリから消しました。GCPプロジェクト自体も削除して完全にクリーンアップしています。

アプリ側もWebhookエンドポイント(api/webhooks/notion-images/)やGCSアップロード処理(lib/gcs/)、@google-cloud/storage パッケージを削除しました。

Claude Codeで短時間移行できた話

今回の移行、実はほぼ全部Claude Codeに実装させました。Terraformのモジュール作成、画像ダウンロードスクリプト、GitHub Actionsのワークフロー、不要コードの削除まで、一通り任せています。

正直、ここまでスムーズにいくとは思っていませんでした。手戻りがほぼなく、短時間で移行が完了しています。

ちなみに前編のWebhook × GCS方式は手動で2日くらいかけて構築していて、エラー対処やら権限修正やらでfixコミットが大量に出ていました。今回はClaude Codeに任せた結果、SSG設定からCloudflareデプロイ、GCPリソース削除まで一気に終わっています。AI駆動開発の恩恵をかなり感じた体験でした。

移行の成果

コスト: 月額約2,800円 → 0円

Cloudflare Pagesの無料プランは月500ビルド・無制限リクエスト・無制限帯域幅なので、個人ブログなら完全に無料で運用できます。

構成のシンプル化

Webhook、GCS、Cloud CDN、Cloud Build、Cloud Runが全部なくなりました。「Notionに書く → GitHub Actionsでビルド → Cloudflare Pagesで配信」だけです。何かあったときにデバッグする箇所も激減しました。

静的配信によるパフォーマンス向上

SSGで事前にHTMLが生成されているので、サーバーサイドの処理が一切ありません。Cloudflareのエッジから直接配信されるので、表示速度もかなり速くなりました。

まとめ

  • 前編のWebhook × GCS方式は動くけど複雑でコストもかかる構成でした
  • SSG化してCloudflare Pagesに移行したら、月額0円でシンプルな構成になりました
  • Claude Codeに実装を任せたら、短時間で手戻りなく移行が完了しました

個人ブログはシンプルな構成が正義だなと改めて感じました。最初からこうすればよかった感はありますが、Webhook × GCSで試行錯誤した経験があったからこそ「SSGでいい」という判断ができたので、回り道も無駄ではなかったと思います。

今後は記事の数を増やしていくことに注力したいです。