プリレンダリングは、クライアントに返す前に、ページの最終 HTML を生成します。JavaScript が多いアプリケーションでは、ブラウザーでページを読み込み、クライアント側の JavaScript の実行を待って、最初のアプリシェルではなく、レンダリング済み HTML を返します。
検索クローラー、ソーシャルプレビューボット、AI のインデックス処理、パートナー連携などが、通常はブラウザー内で作られる HTML を必要とするときに、プリレンダリングが役立ちます。Cloudflare Browser Run と Cloudflare Workers を使うと、管理対象のヘッドレス Chrome で公開 URL をレンダリングし、レンダリング済み HTML を返せます。
このチュートリアルでは、次を行います。
- Worker に Browser Run バインディングを追加する
- 最小構成のプリレンダリングエンドポイントを作る
- Worker がレンダリングできるホスト名を制限する
- リモートモードでエンドポイントをローカル検証する
このチュートリアルを進めるには、次が必要です。
- Cloudflare アカウント
- TypeScript を使う Worker プロジェクト
- プリレンダリングする公開 URL
プリレンダリング対象のページは、どこで動いていても構いません。このチュートリアルの Worker は、Browser Run を呼び出すプリレンダリングサービスとしてだけ動きます。
Wrangler の設定に Browser Run バインディングを追加します。
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "my-prerender-worker",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-09-20",
"browser": {
"binding": "BROWSER"
}
}name = "my-prerender-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-09-20"
[browser]
binding = "BROWSER"src/index.ts の内容を、次の Worker に置き換えます。Worker がプリレンダリングできるホスト名を ALLOWED_HOSTNAMES に入れてください。
// Only render pages you control. This prevents the Worker from becoming
// an open browser-rendering proxy for arbitrary websites.
const ALLOWED_HOSTNAMES = new Set(["example.com", "www.example.com"]);
const getTargetUrl = (request) => {
const requestUrl = new URL(request.url);
const target = requestUrl.searchParams.get("url");
if (!target) {
throw new Error("Missing url query parameter");
}
const targetUrl = new URL(target);
// Only render HTTP(S) pages. Other protocols are not valid web pages.
if (!["http:", "https:"].includes(targetUrl.protocol)) {
throw new Error("Only HTTP and HTTPS URLs are allowed");
}
if (!ALLOWED_HOSTNAMES.has(targetUrl.hostname)) {
throw new Error("This hostname is not allowed");
}
return targetUrl;
};
const renderHtml = async (env, targetUrl) => {
// The /content Quick Actions endpoint loads the page in Browser Run and returns
// a JSON envelope containing the rendered HTML in the result field.
const response = await env.BROWSER.quickAction("content", {
url: targetUrl.toString(),
gotoOptions: {
waitUntil: "networkidle2",
timeout: 30000,
},
// If your page has a specific readiness signal, use waitForSelector
// instead of relying only on network activity.
// waitForSelector: { selector: "[data-prerender-ready='true']", timeout: 30000 },
});
if (!response.ok) {
const detail = (await response.text()).slice(0, 500);
throw new Error(`Browser Run failed with ${response.status}: ${detail}`);
}
const data = await response.json();
if (!data.success || typeof data.result !== "string") {
throw new Error("Browser Run returned an unsuccessful response");
}
return data.result;
};
export default {
async fetch(request, env) {
try {
// Read and validate the URL before sending it to Browser Run.
const targetUrl = getTargetUrl(request);
const html = await renderHtml(env, targetUrl);
// Return the rendered HTML to the crawler or integration.
return new Response(html, {
headers: {
"content-type": "text/html; charset=utf-8",
},
});
} catch (error) {
return Response.json(
{ error: error instanceof Error ? error.message : "Unknown error" },
{ status: 400 },
);
}
},
};interface Env {
BROWSER: BrowserRun;
}
// Only render pages you control. This prevents the Worker from becoming
// an open browser-rendering proxy for arbitrary websites.
const ALLOWED_HOSTNAMES = new Set(["example.com", "www.example.com"]);
const getTargetUrl = (request: Request) => {
const requestUrl = new URL(request.url);
const target = requestUrl.searchParams.get("url");
if (!target) {
throw new Error("Missing url query parameter");
}
const targetUrl = new URL(target);
// Only render HTTP(S) pages. Other protocols are not valid web pages.
if (!["http:", "https:"].includes(targetUrl.protocol)) {
throw new Error("Only HTTP and HTTPS URLs are allowed");
}
if (!ALLOWED_HOSTNAMES.has(targetUrl.hostname)) {
throw new Error("This hostname is not allowed");
}
return targetUrl;
};
const renderHtml = async (env: Env, targetUrl: URL) => {
// The /content Quick Actions endpoint loads the page in Browser Run and returns
// a JSON envelope containing the rendered HTML in the result field.
const response = await env.BROWSER.quickAction("content", {
url: targetUrl.toString(),
gotoOptions: {
waitUntil: "networkidle2",
timeout: 30000,
},
// If your page has a specific readiness signal, use waitForSelector
// instead of relying only on network activity.
// waitForSelector: { selector: "[data-prerender-ready='true']", timeout: 30000 },
});
if (!response.ok) {
const detail = (await response.text()).slice(0, 500);
throw new Error(`Browser Run failed with ${response.status}: ${detail}`);
}
const data = (await response.json()) as {
success: boolean;
result?: string;
};
if (!data.success || typeof data.result !== "string") {
throw new Error("Browser Run returned an unsuccessful response");
}
return data.result;
};
export default {
async fetch(request, env): Promise<Response> {
try {
// Read and validate the URL before sending it to Browser Run.
const targetUrl = getTargetUrl(request);
const html = await renderHtml(env, targetUrl);
// Return the rendered HTML to the crawler or integration.
return new Response(html, {
headers: {
"content-type": "text/html; charset=utf-8",
},
});
} catch (error) {
return Response.json(
{ error: error instanceof Error ? error.message : "Unknown error" },
{ status: 400 },
);
}
},
} satisfies ExportedHandler<Env>;この Worker は url クエリパラメーターを受け取り、ホスト名を検証し、Browser Run にその URL のレンダリングを依頼して、レンダリング済み HTML を返します。
waitUntil: "networkidle2" オプションは、ネットワーク接続が 2 本以下の状態が少なくとも 500 ミリ秒続くまで待ちます。クライアント側で描画するページでは、これで十分なことが多いです。より具体的な準備完了の合図が必要な場合は、同じ Quick Actions のペイロードに waitForSelector を渡し、コンテンツ読み込み後にだけ現れる要素を待ちます。詳細は Browser Run Quick Actions のタイムアウト を参照してください。
-
Worker をリモートモードで起動します。
npx wrangler dev --remoteyarn wrangler dev --remotepnpm wrangler dev --remote.quickAction()メソッドは、ローカル開発モードではまだ使えません。Browser Run の Quick Actions をローカルで試すときは、wrangler dev --remoteを使います。 -
別のターミナルで、レンダリング済みページをリクエストします。
curl "http://localhost:8787/?url=https://example.com/"応答には、対象ページのレンダリング済み HTML が含まれます。
-
ローカル検証のあと、Worker をデプロイします。
npx wrangler deployyarn wrangler deploypnpm wrangler deploy -
デプロイ後、Worker の URL からレンダリング済みページをリクエストします。
curl "https://<YOUR_WORKER_HOSTNAME>/?url=https://example.com/"
- 自分が管理するホスト名だけをレンダリングする
- エッジでのルーティングが必要なときは、最初の受け口として Worker を使う
- クローラーや連携からのリクエストにだけ Browser Run を呼ぶ
- クローラーからの繰り返しリクエストが見込まれる場合は、レンダリング済み HTML をキャッシュする
- 元のコンテンツが変わったら、キャッシュした HTML を再検証する