Cloudflare ハンズオン教科書|2026-08 版
第 4 章

静的アセットと Pages — サイトホスティング

静的サイトも SPA もフルスタックも、Workers に載せる

この章で学ぶこと

Workers の静的アセット機能

前章までの Worker は「コードだけ」をデプロイしていました。Workers の static assets 機能を使うと、HTML・CSS・画像などの静的ファイルを Worker の一部としてアップロードでき、キャッシュと配信は Cloudflare に任せられます。Worker コードと静的アセットは 1 回の wrangler deploy で単一のユニットとしてデプロイされます。

中心になるのは Wrangler 設定ファイルの assets.directory です。ここで指定したフォルダ(フレームワークなら ./public/ や ./dist/、./build/ が定番)の中身が、デプロイ時に Wrangler によって自動アップロードされます。main を持たない「アセットだけの Worker」も作れます。

// wrangler.jsonc — アセットのみの最小構成
{
  "name": "my-static-site",
  "compatibility_date": "2026-08-11",
  "assets": {
    "directory": "./public"
  }
}

配信面の特徴は次の 3 点です。

主な制限は次のとおりです(2026-08 時点)。1 つの Worker に設定できるアセットのコレクションは 1 つだけで、Paid プランのファイル数上限を使うには Wrangler v4.34.0 以降が必要です。

項目Workers FreeWorkers Paid
Worker バージョンあたりのファイル数20,000100,000
ファイル 1 個のサイズ上限25 MiB25 MiB

ルーティング挙動を制御する

静的アセットと Worker スクリプトが同居するとき、リクエストは既定で次の順に処理されます。

  1. リクエストパスがアセットディレクトリ内のファイルに一致すれば、Worker スクリプトを起動せずにそのファイルを配信する(課金なし)
  2. 一致しなければ Worker スクリプトを呼び出す
  3. Worker スクリプトが無ければ 404 Not Found(本文なし)を返す

Worker スクリプトからは env.ASSETS.fetch(request)(assets binding)でアセット配信へ処理を委譲できます。URL のホスト名部分は無視され、パス名だけがアセットの照合に使われます。この「アセット優先」の既定挙動は、assets 配下のオプションで変更できます。

1 本のリクエストがたどる既定の経路 ブラウザ アセット照合 アセットを配信 Worker スクリプトを実行 ここから課金対象 404 Not Found GET /about 一致・Worker 起動なし・無料 不一致・Worker あり 不一致・Worker なし env.ASSETS.fetch() で委譲
図 4-1 無料なのは実線の上段、つまり「照合で一致してそのまま返る」経路だけです。Worker に届くのはアセットに無かったパスだけで、破線の env.ASSETS.fetch() は同じ配信処理に戻る経路ですが、そのリクエストはすでに Worker が起動しているため課金対象です。

not_found_handling — SPA フォールバックとカスタム 404

設定値アセット不一致時の挙動主な用途
(未設定)Worker スクリプトを呼ぶ。無ければ 404フルスタック・API 主体の構成
"single-page-application"/index.html を 200 OK で返すReact / Vue などの SPA
"404-page"最寄りの 404.html を 404 で返すSSG・ドキュメントサイト

SPA モードでは、/dashboard のようにアセットへ一致しない URL への直アクセスにも /index.html が 200 で返り、クライアントサイドルーターが処理を引き継げます。Pages はプロジェクト構成からこの挙動を自動判定していましたが、Workers では誤設定防止のため明示的に設定します。

html_handling — 末尾スラッシュの正規化

既定の auto-trailing-slash は、単独ファイル foo.html を /foo で、フォルダ配下の folder/index.html を /folder/ で配信し、/foo.html や /folder のような非正規 URL は 307 で正規 URL にリダイレクトします。force-trailing-slash(常にスラッシュ付き)・drop-trailing-slash(常にスラッシュなし)・none(正規化を無効化)も選べます。SEO 上の重複コンテンツ問題を防ぐ仕組みです。

run_worker_first — Worker を先に実行する

run_worker_first: true を設定すると、アセットに一致するリクエストでも常に Worker スクリプトが先に実行されます。認証チェック・リクエストログ・HTMLRewriter によるアセット加工といった「ミドルウェア」を実装したいときに使います。配列でパスパターンを渡すと対象ルートを絞り込めます(glob の * と ! による除外パターンに対応。Wrangler v4.20.0 以降)。除外パターンが優先され、記述順は評価に影響しません。

既定(アセット優先) run_worker_first を指定 GET /admin/ アセットに一致 そのまま配信 Worker 起動なし・無料 パス照合 一致 GET /admin/ Worker が先に実行 認証・ログを挟める アセットを配信 env.ASSETS.fetch() 一致しても 認証 OK なら 認証 NG 401 Worker を通らない(無料) 必ず Worker を通る(課金)
図 4-2 左右で変わるのは「アセットに一致した後」です。run_worker_first は経路に Worker を 1 段割り込ませ、401 で止める辺を増やす代わりに、無料で返せていた経路を課金対象に変えます。だからこそ対象パスは配列で絞ります。
// wrangler.jsonc — SPA + API の典型構成
{
  "name": "my-spa-worker",
  "main": "./src/index.ts",
  "compatibility_date": "2026-08-11",
  "assets": {
    "directory": "./dist",
    "not_found_handling": "single-page-application",
    "binding": "ASSETS",
    "run_worker_first": ["/api/*", "!/api/docs/*"]
  }
}

navigation リクエストの最適化

Worker スクリプトがあり、not_found_handling を設定していて、compatibility_date が 2025-04-01 以降(または assets_navigation_prefers_asset_serving フラグ有効)の場合、ブラウザのページ遷移で送られる navigation リクエスト(Sec-Fetch-Mode: navigate ヘッダー付き)は Worker スクリプトを呼ばずにアセットを配信します。SPA のページ遷移のたびに課金対象の Worker 起動が発生するのを防ぐ最適化です。不要なら assets_navigation_has_no_effect フラグで無効化できます。

_headers と _redirects で配信を宣言的に制御する

静的アセットディレクトリの直下に _headers / _redirects という拡張子なしのプレーンテキストファイルを置くと、Workers がそれらを解釈し、静的アセットの応答にカスタムヘッダーとリダイレクトを適用します。ファイル自体は静的アセットとして配信されません。フレームワーク利用時は、ビルドで出力ディレクトリへコピーされる public/ や static/ に置くのが定石です。

1 本のリクエストが通る評価順(run_worker_first なしの既定) 301/302 を返して終了 アセットが無い URL でも効く _redirects 照合 アセット照合 _headers を付与 Worker が応答 2 ファイルは効かない 一致 不一致 一致 応答 不一致 _headers を通らずに応答
図 4-3 2 つのファイルは静的アセットルーターの中の 2 つの段でしかありません。_redirects はアセット照合より手前にあるので、アセットが存在しないパスでも 301/302 が返り Worker まで届きません。逆に Worker まで落ちたリクエストは _headers の段を通らないので、API や SSR のヘッダーは Worker のコードで付けます。

_headers

「URL パターン 1 行 + インデントしたヘッダー行」のブロックで記述します。複数ルールに一致したリクエストはすべてのルールのヘッダーを継承し、既定ヘッダーの除去は ! (感嘆符 + 空白)をヘッダー名に前置します。

# ハッシュ付きファイル名のアセットはブラウザに長期キャッシュさせる
/assets/*
  Cache-Control: public, max-age=31556952, immutable

# 全体に CSP を掛けつつ、JPEG 画像からは除去する
/*
  Content-Security-Policy: default-src 'self';

/*.jpg
  ! Content-Security-Policy

ルールは最大 100 個、1 行あたり 2,000 文字までです(2026-08 時点)。同じヘッダーが複数回適用された場合、値はカンマで結合されます。

_redirects

1 行につき [source] [destination] [code?] を記述します。ステータスコードを省略すると 302 になります。*(splat、転送先では :splat で参照)と :placeholder が使え、ステータスコードに 200 を指定すると同一サイト内のプロキシ(rewrite)として動作します(外部ドメインへのプロキシは不可)。

# 恒久移転は 301 を明示(省略時は 302)
/old-top / 301
/blog/* https://blog.example.com/:splat
/products/:code /items/:code 301

静的リダイレクト 2,000 件 + 動的リダイレクト 100 件の計 2,100 件まで、1 ルール 1,000 文字までです(2026-08 時点)。同一 source に複数ルールがある場合は上にある行が優先され、リダイレクトはヘッダー適用より先に評価されます。またリダイレクトはアセット照合よりも手前で評価されるため、一致するアセットが無いパスでも適用されます。なお、この 2 ファイルのルールが働くのは静的アセット配信の経路だけで、Worker コードが生成したレスポンスには効きません(詳細はつまずきポイント参照)。

フレームワークのデプロイと Pages との関係

React や Astro などのフレームワークで作るサイトも、ビルド出力は「静的アセット(+ SSR 用の Worker スクリプト)」なので同じ仕組みでデプロイできます。C3 の --framework フラグを使うと、フレームワーク込みのプロジェクトを一発で scaffold できます。たとえば React なら npm create cloudflare@latest -- my-react-app --framework=react です。生成されるテンプレートでは worker/index.ts が API バックエンドになり、assets.not_found_handling は single-page-application に設定済み、Cloudflare Vite プラグインにより本番同等の Workers ランタイムでローカル開発できます。Astro・React Router(旧 Remix)・Next.js・Nuxt・SvelteKit など主要フレームワークには Workers 向けガイドとアダプタが用意されています。

Cloudflare Pages との使い分け

Cloudflare Pages は、Git 連携による push 時の自動ビルド・デプロイとプレビュー環境を備えたホスティングサービスで、サーバー処理は Pages Functions が担ってきました。現在は Workers の static assets が Pages の主要機能(無料の静的配信・_headers / _redirects・SPA フォールバック・ASSETS binding・Git 連携ビルド)をカバーしたうえで、Workers でしか使えない機能が多くあります。

機能WorkersPages
Cloudflare Vite プラグイン対応非対応
Gradual Deployments(段階的デプロイ)対応非対応
Cron Triggers対応非対応
Workers Logs / Logpush対応非対応
Durable Objects対応別 Worker 経由の回避策のみ
Queues のコンシューマ対応非対応
Cloudflare 外ネームサーバーのカスタムドメイン非対応対応

公式ドキュメントには Pages から Workers への移行ガイドと機能互換マトリクスが用意されており、Pages docs 側からも「Migrate to Workers」として案内されています。新規開発では Workers を選ぶのが現在の流れです。移行の要点は「pages_build_output_dir を assets.directory へ」「Pages Functions を Worker スクリプトへ」「wrangler pages dev / deploy を wrangler dev / deploy へ」の置き換えです。advanced mode の _worker.js を使っていた場合は、静的アセットとして公開されないよう .assetsignore(.gitignore と同形式)で除外するか、アセットディレクトリの外へ移動します。なお、Pages より前の世代の仕組みである Workers Sites は新規プロジェクトで使わないでください。

ここからのハンズオンでは、静的サイトのデプロイから始めて、「静的サイト + API を 1 つの Worker で配信する」構成までを実際に作ります。3 本とも Free プランで完走できます。

ハンズオン 4-1|静的サイトを Workers にデプロイする

C3 の Static site テンプレートから静的サイトを workers.dev に公開します。所要時間の目安は 10 分です。

  1. プロジェクトを作成する。プロンプトでは Hello World example → Static site → TypeScript → git は Yes → デプロイは No を選ぶ
    npm create cloudflare@latest -- my-static-site
    cd my-static-site
  2. ローカル開発サーバーを起動し、ブラウザで表示を確認する
    npx wrangler dev
    # → http://localhost:8787 で配信される
  3. public/index.html を編集して保存し、リロードで反映されることを確かめたら、デプロイする
    npx wrangler deploy
    # 出力に表示される URL にアクセスして確認する
    #   https://my-static-site.<YOUR_SUBDOMAIN>.workers.dev

<YOUR_SUBDOMAIN> は第 1 章で設定した自分の workers.dev サブドメイン名に読み替えてください。ブラウザで開いて public/index.html の内容が表示されれば成功です。

ハンズオン 4-2|静的サイト + API を 1 つの Worker で配信する

4-1 のプロジェクトに Worker スクリプトを追加し、/api/* は API として、それ以外は静的アセットとして 1 つの Worker から配信します。所要時間の目安は 15 分です。

  1. wrangler.jsonc に main と assets binding を追加する
    {
      "name": "my-static-site",
      "main": "./src/index.ts",
      "compatibility_date": "2026-08-11",
      "assets": {
        "directory": "./public",
        "binding": "ASSETS"
      }
    }
  2. src/index.ts を作成する。アセットに一致しなかったリクエストだけが Worker に届くので、API 以外は assets binding へ委譲する
    interface Env {
      ASSETS: Fetcher;
    }
    
    export default {
      async fetch(request, env): Promise<Response> {
        const url = new URL(request.url);
    
        if (url.pathname === "/api/time") {
          return Response.json({ now: new Date().toISOString() });
        }
    
        // アセット不一致・API 以外はアセット配信へ委譲する
        // (not_found_handling の挙動が適用される)
        return env.ASSETS.fetch(request);
      },
    } satisfies ExportedHandler<Env>;
  3. npx wrangler dev を起動し、両方の配信を確認する
    curl http://localhost:8787/api/time
    # → {"now":"2026-08-11T03:00:00.000Z"} のような JSON
    
    curl http://localhost:8787/
    # → public/index.html の内容(Worker は起動されない)
  4. npx wrangler deploy でデプロイし、workers.dev の URL でも同じ挙動になることを確認する

静的ページへのリクエストは無料のアセット配信、/api/time だけが課金対象の Worker 起動になる、コスト効率のよい構成です。

ハンズオン 4-3|_headers と _redirects で配信を制御する

4-2 のプロジェクトに 2 つの制御ファイルを追加し、ヘッダー付与とリダイレクトを確認します。所要時間の目安は 10 分です。

  1. public/_headers を作成する
    /*
      X-Frame-Options: DENY
      X-Content-Type-Options: nosniff
  2. public/_redirects を作成する
    /old-top / 301
  3. npx wrangler dev を再起動し、curl -I http://localhost:8787/ で 2 つのセキュリティヘッダーが付くこと、curl -I http://localhost:8787/old-top で 301 と location: / が返ることを確認する
  4. curl -I http://localhost:8787/api/time も実行し、Worker が生成する API レスポンスには _headers のヘッダーが付かないことを観察する

_headers / _redirects がアセット応答だけに働くこと、そして両ファイルが配信されないこと(curl http://localhost:8787/_headers が 404 相当になる)まで確認できれば完了です。

つまずきポイント

_headers / _redirects が API や SSR のレスポンスに効かない — この 2 ファイルのルールは静的アセットの応答にのみ適用されます。Worker コードが生成するレスポンス(SSR フレームワークや run_worker_first 経由を含む)には、リクエスト URL がルールに一致していても適用されません。Worker 側では Response にヘッダーを直接設定し、リダイレクトもコードで実装してください。

ブラウザで API パスを開くと JSON ではなく HTML が返る — not_found_handling 設定済み + compatibility_date が 2025-04-01 以降だと、navigation リクエストは Worker を呼ばずアセットを配信します。fetch("/api/date") のようなクライアントサイドリクエストは正常に Worker へ届くため、アプリの動作としては問題ありません。意図的な課金削減の挙動ですが、無効化したい場合は assets_navigation_has_no_effect フラグを設定します。

SPA の深いリンクに直アクセスすると 404 になる — Pages と違い、Workers は SPA かどうかを自動判定しません。assets.not_found_handling に "single-page-application" を明示していないと、アセットに一致しない URL はフォールバックされず 404(または Worker 呼び出し)になります。

Free プランで run_worker_first 対象パスが 429 を返す — run_worker_first のパターンに一致するリクエストは必ず Worker を起動するため、無料枠のリクエスト上限を超えると、アセットへフォールバックせずに 429 Too Many Requests が返ります。! で始まる除外パターンのパスは Worker を経由しないため影響を受けません。

理解度チェック

Q1. 静的アセットに一致したリクエストの課金はどうなる?

無料・無制限です。課金対象になるのは Worker スクリプトが呼び出されたリクエストだけで、アセットの保存にも追加コストはかかりません(2026-08 時点)。

Q2. SPA で、アセットに一致しない URL への直アクセスに index.html を返すための設定は?

assets.not_found_handling を "single-page-application" に設定します。アセット不一致のリクエストに /index.html が 200 OK で返ります。SSG サイトなら "404-page" で最寄りの 404.html を返せます。

Q3. アセット配信よりも先に認証チェックを実行したい。どう設定する?

assets.run_worker_first を使います。true で全リクエスト、配列(glob の * と ! 除外に対応、Wrangler v4.20.0 以降)で特定パスだけ Worker スクリプトを先に実行できます。Worker からは assets binding の env.ASSETS.fetch() でアセット配信に戻します。

Q4. _redirects でステータスコードを省略したときのデフォルトは?

302 です。恒久移転にしたい場合は行末に 301 を明示します。200 を指定すると同一サイト内のプロキシ(rewrite)になります。

Q5. _headers に書いたルールが /api/ のレスポンスに付かないのはなぜ?

_headers / _redirects は静的アセットの応答にのみ適用されるためです。Worker コードが生成するレスポンスには URL が一致していても適用されないので、Worker 内で Response のヘッダーとして付与します。

まとめと次章へ

サイトの「ガワ」を配信できるようになったので、次章からはデータを扱います。第 5 章では、エッジで読み取り最速のグローバル key-value ストア Workers KV を学び、この章で作った構成に動的なデータを持たせていきます。

参考リンク: