Skip to content

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

クローラー向けにページをプリレンダリングする

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

プリレンダリングは、クライアントに返す前に、ページの最終 HTML を生成します。JavaScript が多いアプリケーションでは、ブラウザーでページを読み込み、クライアント側の JavaScript の実行を待って、最初のアプリシェルではなく、レンダリング済み HTML を返します。

検索クローラー、ソーシャルプレビューボット、AI のインデックス処理、パートナー連携などが、通常はブラウザー内で作られる HTML を必要とするときに、プリレンダリングが役立ちます。Cloudflare Browser RunCloudflare Workers を使うと、管理対象のヘッドレス Chrome で公開 URL をレンダリングし、レンダリング済み HTML を返せます。

このチュートリアルでは、次を行います。

  • Worker に Browser Run バインディングを追加する
  • 最小構成のプリレンダリングエンドポイントを作る
  • Worker がレンダリングできるホスト名を制限する
  • リモートモードでエンドポイントをローカル検証する

前提条件

このチュートリアルを進めるには、次が必要です。

  • Cloudflare アカウント
  • TypeScript を使う Worker プロジェクト
  • プリレンダリングする公開 URL

プリレンダリング対象のページは、どこで動いていても構いません。このチュートリアルの Worker は、Browser Run を呼び出すプリレンダリングサービスとしてだけ動きます。

1. 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"

2. プリレンダリング用の Worker を追加する

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 のタイムアウト を参照してください。

3. プリレンダリングをテストする

  1. Worker をリモートモードで起動します。

    npx wrangler dev --remote

    .quickAction() メソッドは、ローカル開発モードではまだ使えません。Browser Run の Quick Actions をローカルで試すときは、wrangler dev --remote を使います。

  2. 別のターミナルで、レンダリング済みページをリクエストします。

    curl "http://localhost:8787/?url=https://example.com/"

    応答には、対象ページのレンダリング済み HTML が含まれます。

4. デプロイする

  1. ローカル検証のあと、Worker をデプロイします。

    npx wrangler deploy
  2. デプロイ後、Worker の URL からレンダリング済みページをリクエストします。

    curl "https://<YOUR_WORKER_HOSTNAME>/?url=https://example.com/"

本番環境での注意点

  • 自分が管理するホスト名だけをレンダリングする
  • エッジでのルーティングが必要なときは、最初の受け口として Worker を使う
  • クローラーや連携からのリクエストにだけ Browser Run を呼ぶ
  • クローラーからの繰り返しリクエストが見込まれる場合は、レンダリング済み HTML をキャッシュする
  • 元のコンテンツが変わったら、キャッシュした HTML を再検証する

関連リソース

役に立ちましたか?