「Cloudflare Pages でブログを作った」と書いていましたが、実体は Workers でした
4月に書いた記事のタイトルが、間違っていました
4月にこのブログを立ち上げたときの記事を書きました。公開時のタイトルは「Astro + Cloudflare Pages でブログをゼロから構築した」です。
このブログ、Cloudflare Pages では動いていません。Cloudflare Workers の静的アセット (Static Assets) です。
気づいたのは先日、まったく別の作業をしていたときでした。ブログに小さな API を1本足そうとして、「Pages Functions を書けばいいか」と functions/ ディレクトリを作りかけたところで手が止まりました。wrangler.toml を開いても、Pages の設定がどこにもない。
しかも、その4月の記事に貼っていた wrangler.toml の例はこれです。
name = "my-blog"
compatibility_date = "2026-04-12"
[assets]
directory = "./dist"
not_found_handling = "404-page"
html_handling = "auto-trailing-slash"
[assets] と書いてあります。これは Workers の書き方です。自分で貼っておいて、4か月気づいていませんでした。
見た目が同じなので、動いている間は気づかない
Pages と Workers の静的アセットは、使っている側からするとほとんど同じ顔をしています。
- どちらも Cloudflare のダッシュボードの「Workers & Pages」に並ぶ
- どちらも GitHub 連携で push すると自動でビルド・デプロイされる
- どちらもカスタムドメインを無料で付けられて、証明書も自動
- どちらも
_headersや_redirectsが使える
静的なページを表示するぶんには違いが出ません。違いが表に出るのは、静的ファイルを返す以外のことをやろうとした瞬間です。今回はそれが「API を1本足す」でした。
出発点が逆、と考えると分かりやすいです。Pages は静的サイトのホスティングが本体で、動的処理は functions/ に置いたファイルが担当します。Workers はスクリプト1本が本体で、そこに静的アセットをぶら下げる形です。だから動的処理は functions/ ではなく、Worker のコードそのものになります。
見分け方は wrangler.toml の1行
一番確実なのは設定ファイルです。Pages の設定にはこのキーがあります。
pages_build_output_dir = "./dist"
Workers の静的アセットはこうです。
main = "./src/worker.ts"
[assets]
directory = "./dist"
pages_build_output_dir があれば Pages、[assets] があれば Workers。Cloudflare のドキュメントにも「main のような Workers 固有の設定キーは Pages Functions の設定ファイルには適用されない」と書かれています。両者は排他です。
ほかに手がかりになるのはこのあたりです。
| 見るところ | Pages | Workers 静的アセット |
|---|---|---|
| wrangler.toml の必須キー | pages_build_output_dir | [assets](+ 動的処理があれば main) |
| 手動デプロイのコマンド | wrangler pages deploy | wrangler deploy |
| 動的処理の置き場所 | functions/ ディレクトリ | main で指定したスクリプト |
| 設定ファイルなしで動くか | 動く(ダッシュボードのビルド設定だけでよい) | 動かない |
リポジトリに設定ファイルが無くて、ビルドコマンドと出力ディレクトリをダッシュボードだけで指定しているなら、それは Pages と考えて大丈夫です。
ちなみに、クライアントにモックを見せるための置き場のほうは本当に Pages でした。こちらの wrangler.toml は pages_build_output_dir の1行だけで、動的処理は functions/ に置いています。同じアカウントの中に両方あったので、余計に混同していました。
Pages Functions を置いても、何も言われません
ここが今回いちばん引っかかった点です。Workers のプロジェクトに functions/ を置くと、エラーも警告も出ないまま無視されます。
手元で最小構成を作って確かめました。wrangler.toml に main と [assets] を書いて、functions/api/hello.ts に Pages Functions のコードを置いた状態です。
$ npx wrangler deploy --dry-run --outdir=out
⛅️ wrangler 4.119.0
────────────────────
✨ Read 1 file from the assets directory .../public
Total Upload: 0.19 KiB / gzip: 0.15 KiB
Your Worker has access to the following bindings:
Binding Resource
env.ASSETS Assets
--dry-run: exiting now.
functions という文字列が1度も出てきません。読み込まれたのはアセットディレクトリの HTML 1ファイルだけです。当然 /api/hello は 404 になります。
厄介なのは、ビルドが成功してしまうところです。デプロイも通るし、サイトも今までどおり表示されます。壊れるのは、置いたはずのエンドポイントを叩いたときだけ。「デプロイは成功したのにエンドポイントが 404」という状態から原因にたどり着くのは、けっこう遠回りになります。
「アセットのディレクトリに入れれば読まれるのでは」と思って public/functions/api/hello.ts に置いてみると、さらによくない結果になりました。
$ curl -i http://127.0.0.1:8799/functions/api/hello.ts
HTTP/1.1 200 OK
Content-Type: video/mp2t
export const onRequest = () => new Response('hello from pages function');
実行されるどころか、ソースコードがそのままダウンロードできる状態です。アセットディレクトリの中身は「そのまま配信するもの」なので当たり前ではあるのですが、.ts が動画 (MPEG-TS) の MIME タイプで返ってくるあたりも含めて、期待と全然違う結果になります。
動的処理は Worker のコードの中に書く
Cloudflare のドキュメントでは、Pages から移行するときは wrangler pages functions build で functions/ を Worker 1本にコンパイルできる、と案内されています。ファイルベースのルーティングを続けたいなら HonoX のような別のフレームワークを検討するよう勧められています。
動的処理が数個で済むなら、Worker の中でパスを見て分岐させるだけで足ります。最小形はこうです。
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// 自分で組み立てて返したいパスだけ先に処理する
if (url.pathname === '/api/hello') {
return Response.json({ message: 'hello' });
}
// それ以外は静的アセットに任せる
return env.ASSETS.fetch(request);
},
};
env.ASSETS.fetch(request) が「アセットを取りに行く」呼び出しです。Pages の middleware における next() にあたる役割を、自分で書くことになります。
もう1つ、[assets] に書ける run_worker_first が「アセットより先に Worker を通すか」の指定です。
[assets]
directory = "./dist"
binding = "ASSETS"
run_worker_first = true
既定は false で、この場合はリクエストが静的アセットに一致したら Worker は呼ばれません。アセットが先で、外れたときだけ Worker に来ます。true にすると、全リクエストがまず Worker を通ります。
["/api/*", "!/api/docs/*"] のように配列でパスを指定して、必要な経路だけ先に通すこともできます。全部を Worker 経由にすると静的ファイルの配信までコードに依存するので、絞れるなら絞ったほうが安全です。
true が要るのは、アセット側のルーティングが返すレスポンスに手を入れたいときです。たとえば html_handling = "auto-trailing-slash" による末尾スラッシュの正規化リダイレクトは、アセットに一致した時点で返ってしまうので、false のままでは Worker から触れません。
_headers はどちらでも使えるが、境界がある
このブログでは public/_headers に X-Frame-Options などのセキュリティヘッダーを書いて、全ページに付けています。これは Pages の機能だと思い込んでいたのですが、Workers の静的アセットでもそのまま使えます。ドキュメントにも「_headers と _redirects は Workers の静的アセットでネイティブにサポートされている」と明記されていました。
ただし境界があります。
Custom headers defined in the
_headersfile are not applied to responses generated by your Worker code(
_headersに定義したカスタムヘッダーは、Worker のコードが生成したレスポンスには適用されません)
実際に curl -sI で見比べると、そのとおりでした。静的アセットとして返っている HTML には _headers に書いた4つが全部付いている一方、Worker が new Response() で組み立てたリダイレクトには1つも付いていません。env.ASSETS.fetch() の戻り値をそのまま返す経路ではヘッダーが残り、自分で組み立てた経路では消えます。Worker から HTML を返すつもりなら、ヘッダーは Worker 側で付け直す必要があります。
ついでに分かったこともあります。_headers に max-age 1年で書いていた HSTS が、実際には Cloudflare のゾーン設定側の値(半年)で返っていました。ファイルに書いた指定は1度も使われていなかったわけで、書いたヘッダーが実際に返っているかは curl -sI で確かめないと分かりません。
Cloudflare の方針: 新しい機能は Workers 側に入る
4月の記事では「Workers と Pages は統合が進んでいる」と書きました。この部分は合っています。ただ、統合の向きはもう少しはっきりしています。
2025年4月の Cloudflare のブログには、こう書かれています。
Cloudflare Pages will continue to be supported, but, going forward, all of our investment, optimizations, and feature work will be dedicated to improving Workers.
(Cloudflare Pages のサポートは続けるが、今後の投資・最適化・機能開発はすべて Workers の改善に充てる)
同じ記事に「Workers が静的アセットの配信とサーバーサイドレンダリングの両方に対応したので、これから始めるなら Workers から始めるべき」ともあります。
公式の対応表を見ると、実際に差がついています。Workers にしかないものはこのあたりです。
- Cron Triggers(定期実行)
- Email Workers、Queue Consumers、Rate Limiting
- Workers Logs、Tail Workers、Logpush、Source Maps(観測まわり)
- 段階的デプロイ、リモート開発モード
- ルート直下以外のパスでの配信
逆に、Pages にしかないものもまだあります。Early Hints、Cloudflare 管理外のゾーンに対するカスタムドメイン、ファイルベースルーティング、Pages Plugins、ブランチデプロイの細かい制御あたりです。今すぐ全部移すべき、という話ではありません。
このブログの場合は Cron Triggers を使っている(予約投稿のために毎日リビルドしている)ので、結果的には Workers 側にいて正解でした。名前を取り違えていただけです。
Pages プロジェクトを持っている人が確認すること
今回の件から、確認しておくといいと思ったものを並べます。
1. wrangler.toml を開いて、pages_build_output_dir があるか見る。 無くて [assets] があるなら Workers です。設定ファイル自体が無いなら Pages です
2. functions/ を置いているなら、そのエンドポイントを実際に叩く。 Workers のプロジェクトなら 404 が返ります。ビルドは成功しているので、叩かないと分かりません
3. _headers に書いたヘッダーが実際に返っているか curl -sI で見る。 Worker が組み立てたレスポンスには付きません。ゾーン設定に上書きされていることもあります
4. 使いたい機能が Workers 側にしかないか対応表で確かめる。 Cron Triggers やログまわりが要るなら、移行を検討する材料になります
5. 記事やドキュメントに「Pages で作った」と書いていないか見る。 今回のこれです
4月の記事は、タイトルと説明文を「Cloudflare Workers」に直したうえで、冒頭に訂正の注記を入れました。公開時のタイトルが何だったかも書いてあります。迷ったのはコードの話ではなくこちらのほうで、黙って書き換えると、読んだ人が「前と違う」となったときに確かめようがありません。かといって間違ったタイトルのまま置いておくのも違う。結局、直した事実が記事の中に残る形にしました。
参照リンク
- Migrate from Pages to Workers | Cloudflare Docs —
functions/の扱い、移行手順 - Workers と Pages の対応表 | Cloudflare Docs — どちらにどの機能があるか
- Headers(
_headers) | Cloudflare Docs — Worker が生成したレスポンスには適用されない件 - HTML handling | Cloudflare Docs — 末尾スラッシュの正規化の仕様
- 静的アセットのバインディングと設定 | Cloudflare Docs —
run_worker_firstの指定方法 - Pages Functions の Wrangler 設定 | Cloudflare Docs —
pages_build_output_dirと、Workers 固有キーが使えない件 - Your frontend, backend, and database — now in one Cloudflare Worker | Cloudflare Blog — 今後の投資は Workers に、という表明(2025年4月8日)
- Pages and Workers are converging into one experience | Cloudflare Blog — 統合を宣言した最初の記事(2023年5月17日)