INTEGRITY Dokumentace

Shell Single Page App (SPA) s bootstrap daty

Tento příklad používá Worker a HTMLRewriter k vložení předem načtených dat API do shellu jednostránkové aplikace (SPA). Worker načítá bootstrapovací data paralelně s HTML shellem a výsledek streamuje do prohlížeče, takže SPA má vše potřebné ještě předtím, než se spustí její JavaScript.

Jsou zobrazeny dvě varianty:

  1. Static Assets : SPA je nasazena pomocí Workers Static Assets
  2. Externí origin : SPA je hostována mimo Cloudflare a Worker před ní stojí jako reverzní proxy, což zlepšuje výkon

Obě varianty používají stejnou techniku vkládání přes HTMLRewriter a stejný vzor zpracování na straně klienta. Vyberte tu, která odpovídá vašemu nasazení.

Tento vzor funguje s libovolným SPA frameworkem, například React, Vue, Svelte nebo jinými. Průvodce nasazením pro konkrétní framework najdete v Webové aplikace.


Možnost 1: Jednostránková aplikace (SPA) postavená celá na Workers

Tuto variantu použijte, pokud je výstup sestavení SPA nasazený jako součást Workeru pomocí Static Assets.

Nakonfigurujte statická aktiva

Nastavte not_found_handling na "single-page-application" tak, aby každá trasa vracela index.html. Použijte run_worker_first pro směrování všech požadavků přes váš Worker s výjimkou hashovaných assets pod /assets/*, které jsou obsluhovány přímo.

{
	"name": "my-spa",
	"main": "src/worker.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"compatibility_flags": ["nodejs_compat"],
	"assets": {
		"directory": "./dist",
		"binding": "ASSETS",
		"not_found_handling": "single-page-application",
		"run_worker_first": ["/*", "!/assets/*"],
	},
}
name = "my-spa"
main = "src/worker.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]

[assets]
directory = "./dist"
binding = "ASSETS"
not_found_handling = "single-page-application"
run_worker_first = [ "/*", "!/assets/*" ]

Podrobnosti o těchto možnostech najdete v Static Assets routing a run_worker_first reference.

Vkládání bootstrap dat pomocí HTMLRewriter

Worker začne okamžitě načítat data z API a poté načte SPA shell ze statických assetů. HTMLRewriter streamuje <head> prohlížeči ihned. Když <body> handler spustí, čeká na odpověď API a na začátek přidá <script> tag obsahující serializovaná data.

Pokud volání API selže, shell se přesto načte a SPA se přepne na načítání dat na straně klienta.

// Env is generated by `wrangler types` — run it whenever you change your config.
// Do not manually define Env — it drifts from your actual bindings.

export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		// Serve root-level static files (favicon.ico, robots.txt) directly.
		// Hashed assets under /assets/* skip the Worker entirely via run_worker_first.
		if (url.pathname.match(/\.\w+$/) && !url.pathname.endsWith(".html")) {
			return env.ASSETS.fetch(request);
		}

		// Start fetching bootstrap data immediately — do not await yet.
		const dataPromise = fetchBootstrapData(env, url.pathname, request.headers);

		// Fetch the SPA shell from static assets (co-located, sub-millisecond).
		const shell = await env.ASSETS.fetch(
			new Request(new URL("/index.html", request.url)),
		);

		// Use HTMLRewriter to stream the shell and inject data into <body>.
		return new HTMLRewriter()
			.on("body", {
				async element(el) {
					const data = await dataPromise;
					if (data) {
						el.prepend(
							`<script>window.__BOOTSTRAP_DATA__=${JSON.stringify(data)}</script>`,
							{ html: true },
						);
					}
				},
			})
			.transform(shell);
	},
};

async function fetchBootstrapData(env, pathname, headers) {
	try {
		const res = await fetch(`${env.API_BASE_URL}/api/bootstrap`, {
			headers: {
				Cookie: headers.get("Cookie") || "",
				"X-Request-Path": pathname,
			},
		});
		if (!res.ok) return null;
		return await res.json();
	} catch {
		// If the API is down, the shell still loads and the SPA
		// falls back to client-side data fetching.
		return null;
	}
}
// Env is generated by `wrangler types` — run it whenever you change your config.
// Do not manually define Env — it drifts from your actual bindings.

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

		// Serve root-level static files (favicon.ico, robots.txt) directly.
		// Hashed assets under /assets/* skip the Worker entirely via run_worker_first.
		if (url.pathname.match(/\.\w+$/) && !url.pathname.endsWith(".html")) {
			return env.ASSETS.fetch(request);
		}

		// Start fetching bootstrap data immediately — do not await yet.
		const dataPromise = fetchBootstrapData(env, url.pathname, request.headers);

		// Fetch the SPA shell from static assets (co-located, sub-millisecond).
		const shell = await env.ASSETS.fetch(
			new Request(new URL("/index.html", request.url)),
		);

		// Use HTMLRewriter to stream the shell and inject data into <body>.
		return new HTMLRewriter()
			.on("body", {
				async element(el) {
					const data = await dataPromise;
					if (data) {
						el.prepend(
							`<script>window.__BOOTSTRAP_DATA__=${JSON.stringify(data)}</script>`,
							{ html: true },
						);
					}
				},
			})
			.transform(shell);
	},
} satisfies ExportedHandler<Env>;

async function fetchBootstrapData(
	env: Env,
	pathname: string,
	headers: Headers,
): Promise<unknown | null> {
	try {
		const res = await fetch(`${env.API_BASE_URL}/api/bootstrap`, {
			headers: {
				Cookie: headers.get("Cookie") || "",
				"X-Request-Path": pathname,
			},
		});
		if (!res.ok) return null;
		return await res.json();
	} catch {
		// If the API is down, the shell still loads and the SPA
		// falls back to client-side data fetching.
		return null;
	}
}

Možnost 2: SPA hostovaná na externím originu

Tuto variantu použijte, pokud jsou váš HTML, CSS a JavaScript nasazené mimo Cloudflare. Worker načte shell SPA z externího originu, pomocí HTMLRewriter do něj vloží bootstrap data a upravenou odpověď streamuje do prohlížeče.

Nakonfigurujte Worker

Protože SPA není součástí Workers Static Assets, nepotřebujete assets blok. Místo toho uložte externí URL adresu origin serveru jako proměnnou prostředí. Připojte Worker ke své doméně pomocí Custom Domain nebo Trasa.

{
	"name": "my-spa-proxy",
	"main": "src/worker.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"compatibility_flags": ["nodejs_compat"],
	"vars": {
		"SPA_ORIGIN": "https://my-spa.example-hosting.com",
		"API_BASE_URL": "https://api.example.com",
	},
}
name = "my-spa-proxy"
main = "src/worker.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]

[vars]
SPA_ORIGIN = "https://my-spa.example-hosting.com"
API_BASE_URL = "https://api.example.com"

Vkládání bootstrap dat pomocí HTMLRewriter

Worker paralelně načítá SPA shell i data z API. Jakmile SPA origin odpoví, HTMLRewriter streamuje HTML a zároveň do něj vkládá bootstrap data do <body>. Statické assety (CSS, JS, obrázky) jsou předávány na externí origin beze změny.

// Env is generated by `wrangler types` — run it whenever you change your config.
// Do not manually define Env — it drifts from your actual bindings.

export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		// Pass static asset requests through to the external origin unmodified.
		if (url.pathname.match(/\.\w+$/) && !url.pathname.endsWith(".html")) {
			return fetch(new Request(`${env.SPA_ORIGIN}${url.pathname}`, request));
		}

		// Start fetching bootstrap data immediately — do not await yet.
		const dataPromise = fetchBootstrapData(env, url.pathname, request.headers);

		// Fetch the SPA shell from the external origin.
		// SPA routers serve index.html for all routes.
		const shell = await fetch(`${env.SPA_ORIGIN}/index.html`);

		if (!shell.ok) {
			return new Response("Origin returned an error", { status: 502 });
		}

		// Use HTMLRewriter to stream the shell and inject data into <body>.
		return new HTMLRewriter()
			.on("body", {
				async element(el) {
					const data = await dataPromise;
					if (data) {
						el.prepend(
							`<script>window.__BOOTSTRAP_DATA__=${JSON.stringify(data)}</script>`,
							{ html: true },
						);
					}
				},
			})
			.transform(shell);
	},
};

async function fetchBootstrapData(env, pathname, headers) {
	try {
		const res = await fetch(`${env.API_BASE_URL}/api/bootstrap`, {
			headers: {
				Cookie: headers.get("Cookie") || "",
				"X-Request-Path": pathname,
			},
		});
		if (!res.ok) return null;
		return await res.json();
	} catch {
		// If the API is down, the shell still loads and the SPA
		// falls back to client-side data fetching.
		return null;
	}
}
// Env is generated by `wrangler types` — run it whenever you change your config.
// Do not manually define Env — it drifts from your actual bindings.

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

		// Pass static asset requests through to the external origin unmodified.
		if (url.pathname.match(/\.\w+$/) && !url.pathname.endsWith(".html")) {
			return fetch(new Request(`${env.SPA_ORIGIN}${url.pathname}`, request));
		}

		// Start fetching bootstrap data immediately — do not await yet.
		const dataPromise = fetchBootstrapData(env, url.pathname, request.headers);

		// Fetch the SPA shell from the external origin.
		// SPA routers serve index.html for all routes.
		const shell = await fetch(`${env.SPA_ORIGIN}/index.html`);

		if (!shell.ok) {
			return new Response("Origin returned an error", { status: 502 });
		}

		// Use HTMLRewriter to stream the shell and inject data into <body>.
		return new HTMLRewriter()
			.on("body", {
				async element(el) {
					const data = await dataPromise;
					if (data) {
						el.prepend(
							`<script>window.__BOOTSTRAP_DATA__=${JSON.stringify(data)}</script>`,
							{ html: true },
						);
					}
				},
			})
			.transform(shell);
	},
} satisfies ExportedHandler<Env>;

async function fetchBootstrapData(
	env: Env,
	pathname: string,
	headers: Headers,
): Promise<unknown | null> {
	try {
		const res = await fetch(`${env.API_BASE_URL}/api/bootstrap`, {
			headers: {
				Cookie: headers.get("Cookie") || "",
				"X-Request-Path": pathname,
			},
		});
		if (!res.ok) return null;
		return await res.json();
	} catch {
		// If the API is down, the shell still loads and the SPA
		// falls back to client-side data fetching.
		return null;
	}
}

Využijte přednačtená data ve vaší SPA

Na klientovi načtěte window.__BOOTSTRAP_DATA__ před jakýmkoli voláním API. Pokud data existují, použijte je přímo. V opačném případě proveďte běžný fetch.

src/App.tsx
// React example — works the same way in Vue, Svelte, or any other framework.
import { useEffect, useState } from "react";

function App() {
	const [data, setData] = useState(window.__BOOTSTRAP_DATA__ || null);
	const [loading, setLoading] = useState(!data);

	useEffect(() => {
		if (data) return; // Already have prefetched data — skip the API call.

		fetch("/api/bootstrap")
			.then((res) => res.json())
			.then((result) => {
				setData(result);
				setLoading(false);
			});
	}, []);

	if (loading) return <LoadingSpinner />;
	return <Dashboard data={data} />;
}

Přidejte deklaraci typu, aby TypeScript rozpoznal globální vlastnost:

global.d.ts
declare global {
	interface Window {
		__BOOTSTRAP_DATA__?: unknown;
	}
}

Další techniky injektáže

Můžete zřetězit více handlerů HTMLRewriter a vkládat tak více než jen bootstrap data.

Nastavit meta tagy

Vložte Open Graph nebo jiné <meta> tagy na základě cesty požadavku. Díky tomu dostávají prohledávače sociálních sítí správné náhledy bez nutnosti plnohodnotného frameworku pro server-side rendering.

new HTMLRewriter()
	.on("head", {
		element(el) {
			el.append(`<meta property="og:title" content="${title}" />`, {
				html: true,
			});
		},
	})
	.transform(shell);

Přidání CSP nonce

Vygenerujte nonce pro každý požadavek a vložte ji do hlavičky Content-Security-Policy i do každého vloženého <script> .

const nonce = crypto.randomUUID();

const response = new HTMLRewriter()
	.on("script", {
		element(el) {
			el.setAttribute("nonce", nonce);
		},
	})
	.transform(shell);

response.headers.set(
	"Content-Security-Policy",
	`script-src 'nonce-${nonce}' 'strict-dynamic';`,
);

return response;

Vložte uživatelskou konfiguraci

Zpřístupněte feature flags nebo nastavení specifická pro dané prostředí aplikaci SPA bez potřeby dalšího volání API.

new HTMLRewriter()
	.on("body", {
		element(el) {
			el.prepend(
				`<script>window.__APP_CONFIG__=${JSON.stringify({
					apiBase: env.API_BASE_URL,
					featureFlags: { darkMode: true },
				})}</script>`,
				{ html: true },
			);
		},
	})
	.transform(shell);