シングルページアプリケーション(SPA)は、クライアントサイドレンダリング(CSR)の Web アプリケーションです。React、Vue、Svelte などのフレームワークで構築することがよくあります。これらのフレームワークのビルドでは、1 つの /index.html ファイルと、それに付随するクライアントサイドのリソース(JavaScript バンドル、CSS スタイルシート、画像、フォントなど)が生成されます。データは通常、クライアントが API へクライアントサイドリクエストを送って取得します。
single-page-application モードを設定すると、Cloudflare はデフォルトのルーティング動作を提供します。ほかのアセットに一致しないナビゲーションリクエスト(Sec-Fetch-Mode: navigate ヘッダー付き)に対して、/index.html ファイルを自動で配信します。どのパスで Worker スクリプトを呼び出すかをより細かく制御するには、高度なルーティング制御 を使えます。
シングルページアプリケーションを Workers にデプロイするには、Wrangler 設定ファイル で assets.directory と assets.not_found_handling オプションを設定します。
{
"name": "my-worker",
// Set this to today's date
"compatibility_date": "2026-09-20",
"assets": {
"directory": "./dist/",
"not_found_handling": "single-page-application"
}
}name = "my-worker"
# Set this to today's date
compatibility_date = "2026-09-20"
[assets]
directory = "./dist/"
not_found_handling = "single-page-application"assets.not_found_handling を single-page-application に設定すると、静的アセット向けの Workers のデフォルト配信動作が上書きされます。受信リクエストが assets.directory 内のファイルに一致しない場合、Workers は /index.html の内容を 200 OK ステータスで配信します。
Worker スクリプト(main)があり、assets.not_found_handling を設定しており、かつ assets_navigation_prefers_asset_serving compatibility flag を使っている(または compatibility date を 2025-04-01 以降にしている)場合、ナビゲーションリクエスト は Worker スクリプトを呼び出しません。ナビゲーションリクエスト は、Sec-Fetch-Mode: navigate ヘッダー付きのリクエストです。ブラウザーはページへ遷移するときに、このヘッダーを自動で付けます。これにより Worker スクリプトの課金対象の呼び出しが減ります。クライアント側が中心のアプリケーションでは、そうしなければ Worker スクリプトが不要なほど頻繁に呼び出されるため、特に有効です。
ナビゲーションリクエストから Worker スクリプトへ値を渡す必要がある場合があります。たとえば OAuth コールバックとして動作させるとき、/oauth/callback?code=... のようなルートへのリクエストを想定することがあります。assets_navigation_prefers_asset_serving フラグがある場合、Worker スクリプトではなく HTML アセットが配信されます。この場合は、該当ルート向けのクライアントアプリケーション内、またはエンドポイント専用の簡易 HTML ファイルで、クライアント側の JavaScript からサーバーへ値を渡すことを推奨します。
<!DOCTYPE html>
<html>
<head>
<title>OAuth callback</title>
</head>
<body>
<p>Loading...</p>
<script>
(async () => {
const response = await fetch("/api/oauth/callback" + window.location.search);
if (response.ok) {
window.location.href = '/';
} else {
document.querySelector('p').textContent = 'Error: ' + (await response.json()).error;
}
})();
</script>
</body>
</html>import { WorkerEntrypoint } from "cloudflare:workers";
export default class extends WorkerEntrypoint {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === "/api/oauth/callback") {
const code = url.searchParams.get("code");
const sessionId =
await exchangeAuthorizationCodeForAccessAndRefreshTokensAndPersistToDatabaseAndGetSessionId(
code,
);
if (sessionId) {
return new Response(null, {
headers: {
"Set-Cookie": `sessionId=${sessionId}; HttpOnly; SameSite=Strict; Secure; Path=/; Max-Age=86400`,
},
});
} else {
return Response.json(
{ error: "Invalid OAuth code. Please try again." },
{ status: 400 },
);
}
}
return new Response(null, { status: 404 });
}
}import { WorkerEntrypoint } from "cloudflare:workers";
export default class extends WorkerEntrypoint {
async fetch(request: Request) {
const url = new URL(request.url);
if (url.pathname === "/api/oauth/callback") {
const code = url.searchParams.get("code");
const sessionId = await exchangeAuthorizationCodeForAccessAndRefreshTokensAndPersistToDatabaseAndGetSessionId(code);
if (sessionId) {
return new Response(null, {
headers: {
"Set-Cookie": `sessionId=${sessionId}; HttpOnly; SameSite=Strict; Secure; Path=/; Max-Age=86400`,
},
});
} else {
return Response.json(
{ error: "Invalid OAuth code. Please try again." },
{ status: 400 }
);
}
}
return new Response(null, { status: 404 });
}
}SPA のルーティング動作をより明示的に制御するには、ルートパターンの配列を指定した run_worker_first を使います。この方法では、Sec-Fetch-Mode: navigate の自動検出が無効になり、どのリクエストを Worker スクリプトで処理し、どれを静的アセットとして配信するかを明示的に決められます。
{
"name": "my-worker",
// Set this to today's date
"compatibility_date": "2026-09-20",
"main": "./src/index.ts",
"assets": {
"directory": "./dist/",
"not_found_handling": "single-page-application",
"binding": "ASSETS",
"run_worker_first": ["/api/*", "!/api/docs/*"]
}
}name = "my-worker"
# Set this to today's date
compatibility_date = "2026-09-20"
main = "./src/index.ts"
[assets]
directory = "./dist/"
not_found_handling = "single-page-application"
binding = "ASSETS"
run_worker_first = [ "/api/*", "!/api/docs/*" ]この設定では、ブラウザーのナビゲーションヘッダーに依存せず、ルーティングを明示的に制御できます。細かいルーティングが必要な複雑な SPA に適しています。Worker スクリプトは一致したルートを処理し、(任意で アセットバインディング を使い)動的コンテンツを配信できます。
例:
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/api/name") {
return new Response(JSON.stringify({ name: "Cloudflare" }), {
headers: { "Content-Type": "application/json" },
});
}
return new Response(null, { status: 404 });
},
};export default {
async fetch(request, env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/api/name") {
return new Response(JSON.stringify({ name: "Cloudflare" }), {
headers: { "Content-Type": "application/json" },
});
}
return new Response(null, { status: 404 });
},
} satisfies ExportedHandler;run_worker_first を使い、SPA シェルがブラウザーに届く前にデータを埋め込むこともできます。HTMLRewriter で API データを先読みし、HTML ストリームに埋め込む完全な例は ブートストラップデータを持つ SPA シェル を参照してください。
Vite ベースの SPA フレームワークを使っている場合は、Vite ネイティブな開発体験を提供する Vite plugin が役立つことがあります。
多くの場合、assets.not_found_handling を single-page-application に設定すれば、期待する動作になります。独自フレームワークを作っている場合や、特別な要件がある場合は、次の図でルーティング判定の詳細を確認できます。
ルーティング判定の全体図
flowchart
Request@{ shape: stadium, label: "受信リクエスト" }
Request-->RunWorkerFirst
RunWorkerFirst@{ shape: diamond, label: "Worker スクリプトを先に実行する?" }
RunWorkerFirst-->|リクエストが run_worker_first のパスに一致|WorkerScriptInvoked
RunWorkerFirst-->|リクエストが run_worker_first の否定パスに一致|AssetServing
RunWorkerFirst-->|一致なし|RequestMatchesAsset
RequestMatchesAsset@{ shape: diamond, label: "リクエストがアセットに一致する?" }
RequestMatchesAsset-->|はい|AssetServing
RequestMatchesAsset-->|いいえ|WorkerScriptPresent
WorkerScriptPresent@{ shape: diamond, label: "Worker スクリプトがある?" }
WorkerScriptPresent-->|いいえ|AssetServing
WorkerScriptPresent-->|はい|RequestNavigation
RequestNavigation@{ shape: diamond, label: "ナビゲーションリクエスト?" }
RequestNavigation-->|いいえ|WorkerScriptInvoked
WorkerScriptInvoked@{ shape: rect, label: "Worker スクリプトを呼び出し" }
WorkerScriptInvoked-.->|アセットバインディング|AssetServing
RequestNavigation-->|はい|AssetServing
subgraph assetServing ["アセット配信"]
AssetServing@{ shape: diamond, label: "リクエストがアセットに一致する?" }
AssetServing-->|はい|AssetServed
AssetServed@{ shape: stadium, label: "**200 OK**<br />アセットを配信" }
AssetServing-->|いいえ|NotFoundHandling
subgraph single-page-application
NotFoundHandling@{ shape: rect, label: "リクエストを /index.html に書き換え" }
NotFoundHandling-->SPAExists
SPAExists@{ shape: diamond, label: "HTML ページがある?" }
SPAExists-->|はい|SPAServed
SPAExists-->|いいえ|Generic404PageServed
Generic404PageServed@{ shape: stadium, label: "**404 Not Found**<br />空の本文レスポンスを配信" }
SPAServed@{ shape: stadium, label: "**200 OK**<br />/index.html ページを配信" }
end
end課金対象になるのは、Worker スクリプトが呼び出された場合だけです。そのあと、アセットバインディングを使ってアセットを配信できます(上の図の点線)。
SPA の配信に影響することは少ないですが、アセットの一致方法の詳細は HTML 処理のドキュメント を参照してください。