effidevFlutter・Cloudflareエッジ・クラウドコスト最適化
日本語

Cloudflare Pages→Workers Static Assets移行ガイド

Cloudflare PagesからWorkers Static Assetsへの移行アーキテクチャ

2025年末からCloudflareは公式にPagesをメンテナンスモード(maintenance mode)へ移行し、今後のすべての投資と新機能をWorkersプラットフォームに集中させると発表しました。既存のPagesプロジェクトは引き続き動作しますが、Cron Triggers、強化されたObservability(可観測性)、Durable Objectsの直接バイン딩などの新機能はWorkersでのみ提供されます。

この記事では、Cloudflare Pagesで運用中の静的サイトやフルスタックアプリをWorkers Static Assetsへ無停止(ダウンタイムなし)で移行する全手順を解説します。実際にこのブログ(effidev.dev)が現在 pages_build_output_dir: "dist" で運用中のPagesプロジェクトであるため、同じ移行を実践する視点から執筆しました。

要点まとめ

  • Pagesはメンテナンスモード: 新機能や最適化への投資が停止しました。Workersが静的資産 + 動的ロジックを単一のデプロイ単位で配信する公式推奨方式です。
  • 静的資産リクエストは無料・無制限: Workers Static Assetsを通じて配信される静的ファイル(HTML、CSS、画像)はWorkerの呼び出し(invocation)としてカウントされないため、料金が発生しません
  • ファイル制限10万個: 有料プラン基準でWorkerバージョンあたり最大100,000ファイル、個別ファイル25MiBまでサポートされています(Wrangler 4.34.0+)。
  • wrangler.jsoncの変更は最小限: pages_build_output_dirassets.directoryに置き換えることで核心となる切り替えが完了します。
  • フレームワークアダプターの統合: Astro、Next.js、SvelteKitなどのSSRフレームワークは、Cloudflare Workers互換アダプターを設定するだけで、静的資産とサーバーサイドロジックが1つのWorkerとしてデプロイされます。

1. Pages vs Workers Static Assets: 何が変わるのか

Cloudflare公式移行ガイドで明示されている主な違いです。

比較項目 Cloudflare Pages Workers Static Assets
プラットフォーム状態 メンテナンスモード (新機能停止) 活発な開発 + 投資集中
静的資産配信 内蔵 assets.directory 設定で同等サポート
サーバーサイドロジック _worker.js (制限あり) フルWorkerスクリプト (無制限バインディング)
Durable Objects 직접 바인딩 불가 / 直接バインディング不可 직접 바인딩 가능 / 直接バインディング可能
Cron Triggers 非対応 対応
Observability 基本ログのみ Workers Logs, Tail Workers, Logpush
ファイル制限 20,000個 100,000個 (有料プラン)
デプロイコマンド wrangler pages deploy wrangler deploy
静的資産料金 無料 無料 (Worker呼び出しカウント外)

決定的な違いはDurable ObjectsとCron Triggersの直接バインディングです。Pagesではこれらを別々のWorkerに分離する必要がありましたが、Workers Static Assetsでは1つの wrangler.jsonc で静的資産の配信 + DO + Cron + D1 + R2 をすべて設定できます。

2. 移行前のチェックリスト

移行前に確認すべき項目です。1つでも満たされていない場合、移行後に障害が発生する可能性があります。

チェックリスト 確認方法
Wranglerバージョン ≥ 4.34.0 npx wrangler --version
ビルド出力ディレクトリの確認 ls dist/ またはフレームワーク別のビルド出力パス
_worker.js の使用有無 Pages advanced modeで使用している場合、Workerスクリプトへの移行が必要
カスタムドメインDNS設定 Pagesプロジェクト의 CNAMEレコードをWorkersルートへ再設定
_headers / _redirects 파일 / ファイル Workersでは動作しない → Workerスクリプト内のロジックへ移行
環境変数 / Secrets Pagesダッシュボード → wrangler.jsonc[vars] または wrangler secret put
# 現在のwranglerバージョンを確認
npx wrangler --version
# → 4.34.0以上で100,000ファイル制限をサポート

# Pagesプロジェクトの現在の設定を確認
cat wrangler.jsonc

3. 核心となる移行:wrangler.jsonc の変換

既存のPages設定からWorkers Static Assetsへ移行するための最小限のdiffです。

// wrangler.jsonc
{
  "name": "effidev",
  "compatibility_date": "2026-08-04",
- "pages_build_output_dir": "dist"
+ "main": "src/worker.ts",
+ "assets": {
+   "directory": "./dist",
+   "binding": "ASSETS"
+ }
}

この変更により、従来 wrangler pages deploy dist でデプロイしていたものが wrangler deploy に変わります。静的資産は ./dist ディレクトリから自動的にアップロードされ、Workerスクリプト(src/worker.ts)で動的ロジックを処理します。

Workerスクリプト (src/worker.ts)

静的サイトのみを配信する最もシンプルなWorkerのエントリーポイントです。

// src/worker.ts
// 静的資産のみを配信する最小限のWorker — 動的ロジックがなければこれだけで十分です
export default {
  async fetch(
    request: Request,
    env: { ASSETS: Fetcher },
  ): Promise<Response> {
    // すべてのリクエストを静的資産バインディングへ転送
    return env.ASSETS.fetch(request);
  },
};

落とし穴に注意: assets.run_worker_firsttrue に設定すると、すべてのリクエストがWorkerスクリプトを経由し、Workerの呼び出し(invocation)料金が発生します。静的サイトの場合はデフォルト値(false)を維持する必要があります — この場合、静的資産のリクエストはWorkerをスキップして直接配信されるため無料です。

4. _headers_redirects → Workerスクリプトへの移行

Pagesで使用していた _headers_redirects ファイルはWorkersでは動作しません。Workerスクリプト内で直接処理する必要があります。

// src/worker.ts — ヘッダー・リダイレクト統合バージョン
export default {
  async fetch(
    request: Request,
    env: { ASSETS: Fetcher },
  ): Promise<Response> {
    const url = new URL(request.url);

    // _redirectsの代替: 従来のURLパターンを新しいパスへ301リダイレクト
    const redirects: Record<string, string> = {
      '/old-blog/': '/ko/blog/',
      '/legacy-page/': '/ko/',
    };

    const redirect = redirects[url.pathname];
    if (redirect) {
      return Response.redirect(new URL(redirect, request.url).toString(), 301);
    }

    // 静的資産の取得
    const response = await env.ASSETS.fetch(request);

    // _headersの代替: レスポンスヘッダーのカスタマイズ
    const headers = new Headers(response.headers);

    // セキュリティヘッダーの追加
    headers.set('X-Content-Type-Options', 'nosniff');
    headers.set('X-Frame-Options', 'DENY');
    headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');

    // キャッシュポリシー: 画像は1年、HTMLは10分
    if (url.pathname.match(/\.(webp|png|jpg|svg|woff2)$/)) {
      headers.set('Cache-Control', 'public, max-age=31536000, immutable');
    } else if (url.pathname.endsWith('.html') || url.pathname.endsWith('/')) {
      headers.set('Cache-Control', 'public, max-age=600, s-maxage=3600');
    }

    return new Response(response.body, {
      status: response.status,
      headers,
    });
  },
};

このWorkerでは assets.run_worker_firsttrue に設定する必要があります — ヘッダー操作が必要なためです。その代わりWorkerの呼び出し(invocation)料金が発生しますが、Paidプランでは月間1,000万リクエストが$5に含まれているため、ほとんどの静的サイトでは十分です。

// wrangler.jsonc — run_worker_firstの有効化
{
  "name": "effidev",
  "compatibility_date": "2026-08-04",
  "main": "src/worker.ts",
  "assets": {
    "directory": "./dist",
    "binding": "ASSETS",
    "run_worker_first": true
  }
}

5. SSRフレームワークアダプターの設定 (Astro・Next.js・SvelteKit)

このブログのようにAstroを使用する静的サイトは、別途アダプターなしでビルド出力のみを assets.directory に指定するだけで動作します。しかし、SSRを使用するフレームワークの場合はCloudflare Workers互換アダプターが必要です。

Astro (静的ビルド — 最もシンプル)

# astro.config.mjs — アダプターなしの静的ビルド
# output: 'static'(デフォルト値)の場合、dist/ にHTMLが直接生成されます
npm run build
# → dist/ フォルダを wrangler.jsonc の assets.directory に指定

Astro (SSRモード)

npx astro add cloudflare
// astro.config.mjs — Cloudflare Workers SSRアダプター
import cloudflare from '@astrojs/cloudflare';

export default defineConfig({
  output: 'server',
  adapter: cloudflare({
    platformProxy: { enabled: true },
  }),
});

Next.js (opennextjs-cloudflare)

npx create-next-app@latest --typescript
npm install @opennextjs/cloudflare

Next.js 16 "use cache" RSCストリーミングガイドで解説した "use cache" ディレクティブとRSCストリーミングも、Workersアダプターを通じて正常に動作します。

6. Durable Objects・Cron・D1・R2の統合デプロイ

Pagesでは不可能だった核心機能です。Workers Static Assetsに移行すると、1つの wrangler.jsonc で静的サイト + バックエンドバインディングをすべて管理できます。

// wrangler.jsonc — フルスタック統合設定
{
  "name": "effidev",
  "compatibility_date": "2026-08-04",
  "main": "src/worker.ts",
  "assets": {
    "directory": "./dist",
    "binding": "ASSETS",
    "run_worker_first": true
  },
  // D1 データベース
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "effidev-analytics",
      "database_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    }
  ],
  // R2 オブジェクトストレージ
  "r2_buckets": [
    {
      "binding": "BUCKET",
      "bucket_name": "effidev-media"
    }
  ],
  // Cron Triggers (Pagesでは不可能でした!)
  "triggers": {
    "crons": ["0 */6 * * *"]
  }
}

AWS S3からCloudflare R2への移行ガイドで解説したR2バケットバインディングも、別個のWorkerを用意することなくブログのWorkerと統合できます。

// src/worker.ts — Cron + D1 + R2 統合ハンドラー
interface Env {
  ASSETS: Fetcher;
  DB: D1Database;
  BUCKET: R2Bucket;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    // APIエンドポイントはWorkerが直接処理
    if (url.pathname.startsWith('/api/')) {
      return handleApi(request, env);
    }

    // その他は静的資産を配信
    return env.ASSETS.fetch(request);
  },

  // 6時間ごとに実行されるCron(Pagesでは別個のWorkerが必要でした!)
  async scheduled(
    _controller: ScheduledController,
    env: Env,
  ): Promise<void> {
    // D1から analytics データを集計
    const result = await env.DB.prepare(
      'SELECT COUNT(*) as total FROM page_views WHERE date = date("now")',
    ).first();

    // R2に日次レポートを保存
    await env.BUCKET.put(
      `reports/${new Date().toISOString().slice(0, 10)}.json`,
      JSON.stringify(result),
    );
  },
};

async function handleApi(request: Request, env: Env): Promise<Response> {
  // APIロジックの実装
  return new Response(JSON.stringify({ status: 'ok' }), {
    headers: { 'Content-Type': 'application/json' },
  });
}

7. デプロイコマンドの変更とDNS切り替え

デプロイコマンド

# Before (Pages)
npx wrangler pages deploy dist --project-name=effidev

# After (Workers Static Assets)
npx wrangler deploy

Workersのデプロイは wrangler.jsoncname フィールドをプロジェクト名として使用します。

DNS切り替え (カスタムドメイン)

PagesプロジェクトのカスタムドメインはCNAMEで <project>.pages.dev を指しています。Workersへ移行する際、Cloudflareダッシュボードの Workers → ルート(Route)でドメインを再接続する必要があります。

ステップ 対応内容
1. Workersデプロイ wrangler deploy でWorker + 静的資産をデプロイ
2. カスタムドメイン接続 Cloudflareダッシュボード → Workers → 対象のWorker → Settings → Domains & Routes → Add Custom Domain
3. Pagesカスタムドメイン解除 Pagesプロジェクトからカスタムドメインを削除(DNS衝突を防止)
4. 検証 curl -sI https://yourdomain.com/ | head -3 → 200を確認

落とし穴: DNS切り替え時にPagesとWorkersが同じドメインを同時にバインディングするとルーティング衝突が発生します。必ずPagesのドメインを先に解除してからWorkersのルートを追加してください。

8. .assetsignore で不要なファイルを外す

ビルド出力ディレクトリにデプロイしたくないファイルがある場合は .assetsignore を使用します。.gitignore と同じ構文です。

# dist/.assetsignore
_worker.js        # Pages advanced modeの残骸 — Worker資産としてアップロードしてはいけません
*.map             # ソースマップはプロダクションにアップロードしません
.DS_Store

よくある質問

既存のPagesプロジェクトをすぐに移行する必要がありますか?

いいえ、必要ありません。既存のPagesプロジェクトは引き続き動作します。ただしCron Triggers、Durable Objectsの直接バインディング、Workers Logsなどの新機能が必要な場合や、ファイル数が20,000個を超える大規模プロジェクトの場合は移行をお勧めします。

静的サイトなのにWorkerの呼び出し(invocation)料金が発生しますか?

assets.run_worker_first をデフォルト値(false)のままにしておけば、静的資産のリクエストはWorkerを経由しないため呼び出し料金は0円です。ヘッダー操作やAPIロジックが必要で true に設定した場合でも、Paidプラン($5/月)に1,000万リクエストが含まれているため、ほとんどの静的サイトでは追加費用が発生しません。

_headers_redirects ファイルはどうなりますか?

Workersでは動作しません。Workerスクリプト内で直接ヘッダー設定とリダイレクトロジックを実装する必要があります(§4参照)。コードで管理されるため、むしろバージョン管理やテストが容易になるというメリットがあります。

Pages Functions(/functions/)を使っていたのですが、どのように移行しますか?

Pages Functionsの各ファイルをWorkerスクリプトのルーティングロジックへ統合します。Workersでは単一のエントリーポイント(main)でURLパターンに応じてハンドラーを分岐する構造になります。Honoのような軽量ルーターフレームワークを使用すれば、従来のFunctions構造と類似したファイルベースのルーティングを維持できます。