Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

静的サイト生成(SSG)とカスタム 404 ページ

最終更新 Markdown で表示Agent セットアップ

静的サイト生成(SSG)アプリケーションは、あらかじめビルド(「事前レンダリング」)されることが中心の Web アプリケーションです。GatsbyDocusaurus などのフレームワークで作ることが多いです。これらのフレームワークのビルドでは、多数の HTML ファイルと、付随するクライアント側リソース(JavaScript バンドル、CSS スタイルシート、画像、フォントなど)が出力されます。データは静的か、ビルド時に取得して HTML に組み込むか、クライアントが API へクライアント側リクエストで取得します。

SSG フレームワークでは、カスタム 404 ページを作れることがよくあります。

設定

静的サイト生成アプリケーションを Workers にデプロイするには、Wrangler 設定ファイルassets.directory を設定します。必要に応じて assets.not_found_handlingassets.html_handling も設定します。

{
	"name": "my-worker",
	// Set this to today's date
	"compatibility_date": "2026-09-20",
	"assets": {
		"directory": "./dist/",
		"not_found_handling": "404-page",
		"html_handling": "auto-trailing-slash"
	}
}
name = "my-worker"
# Set this to today's date
compatibility_date = "2026-09-20"

[assets]
directory = "./dist/"
not_found_handling = "404-page"
html_handling = "auto-trailing-slash"

assets.html_handling のデフォルトは auto-trailing-slash で、通常はこれで期待どおりの動作になります。個別ファイル(foo.html など)は末尾スラッシュ なし で配信され、フォルダーのインデックスファイル(foo/index.html など)は末尾スラッシュ あり で配信されます。あるいは、HTML ページへのリクエストで末尾スラッシュを強制する(force-trailing-slash)か、末尾スラッシュを除く(drop-trailing-slash)こともできます。

カスタム 404 ページ

assets.not_found_handling404-page にすると、静的アセットに対する Workers のデフォルト配信動作が上書きされます。受信リクエストが assets.directory 内のファイルに一致しない場合、Workers は最も近い 404.html の内容を 404 Not Found ステータスで返します。

ナビゲーションリクエスト

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 からサーバーへ値を渡すことを推奨します。

./dist/oauth/callback.htmlhtml
<!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>
./worker/index.jsjs
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 });
	}
}
./worker/index.tsts
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 });
	}
}

ローカル開発

Vite ベースの SPA フレームワークを使っている場合は、Vite ネイティブな開発体験を提供する Vite plugin が役立つことがあります。

リファレンス

多くの場合、assets.not_found_handling404-page にすれば期待どおりの動作になります。独自フレームワークを作っている場合や特別な要件がある場合は、次の図でルーティング判断の詳細を確認できます。

ルーティング判断の全体図
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 Asset serving
	AssetServing@{ shape: diamond, label: "リクエストがアセットに一致しますか?" }
	AssetServing-->|はい|AssetServed
	AssetServed@{ shape: stadium, label: "**200 OK**<br />アセットを配信" }
	AssetServing-->|いいえ|NotFoundHandling

	subgraph 404-page
		NotFoundHandling@{ shape: rect, label: "リクエストを ../404.html に書き換え" }
		NotFoundHandling-->404PageExists
		404PageExists@{ shape: diamond, label: "HTML ページがありますか?" }
		404PageExists-->|はい|404PageServed
		404PageExists-->|いいえ|404PageAtIndex
		404PageAtIndex@{ shape: diamond, label: "ルートの /404.html へのリクエストですか?" }
		404PageAtIndex-->|はい|Generic404PageServed
		404PageAtIndex-->|いいえ|NotFoundHandling
		Generic404PageServed@{ shape: stadium, label: "**404 Not Found**<br />空ボディのレスポンスを配信" }
		404PageServed@{ shape: stadium, label: "**404 Not Found**<br />404.html を配信" }
	end

end

課金対象になるのは、Worker スクリプトが呼び出された場合だけです。そこから、アセットバインディングを使ってアセットを配信できます(上図の点線)。

アセットの一致方法の詳細は、HTML 処理のドキュメント を参照してください。

役に立ちましたか?