← Browser Run / browser-run / how-to
Предварительный рендеринг страниц для краулеров
Предварительный рендеринг создаёт итоговый HTML страницы до того, как она отправляется клиенту. Для приложений с большим количеством JavaScript это означает загрузку страницы в браузере, ожидание выполнения клиентского JavaScript и возврат отрендеренного HTML вместо исходного каркаса приложения.
Предварительный рендеринг полезен, когда поисковым краулерам, ботам социальных превью, заданиям ИИ-индексации или партнёрским интеграциям нужен HTML-контент, который ваше приложение обычно создаёт в браузере. С помощью Cloudflare Browser Run и Cloudflare Workers, вы можете отрендерить публичный URL-адрес в управляемом headless-браузере Chrome и вернуть отрендеренный HTML.
В этом руководстве вы:
- Добавьте привязку Browser Run к Worker
- Создайте минимальный эндпоинт предварительного рендеринга
- Ограничьте хосты, которые Worker может рендерить
- Протестируйте эндпоинт локально в удалённом режиме
Предварительные требования
Чтобы следовать этому руководству, вам понадобится:
- Аккаунт Cloudflare
- Worker-проект, использующий TypeScript
- Публичный URL для предварительного рендеринга
Страница, которую вы предварительно рендерите, может размещаться где угодно. Worker в этом руководстве выступает лишь сервисом предварительного рендеринга, который вызывает Browser Run.
1. Настройка Browser Run
Добавьте привязку Browser Run в конфигурацию Wrangler:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "my-prerender-worker",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-08-28",
"browser": {
"binding": "BROWSER"
}
}name = "my-prerender-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
[browser]
binding = "BROWSER"2. Добавление Worker для предварительного рендеринга
Замените содержимое src/index.ts со следующим Worker. Обновите ALLOWED_HOSTNAMES чтобы указать имена хостов, для которых ваш Worker может выполнять предварительный рендеринг.
// 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" параметр ожидает, пока на странице не останется не более двух сетевых подключений в течение как минимум 500 мс. Обычно этого достаточно для страниц, отрисовываемых на стороне клиента. Если вашей странице требуется более точный сигнал готовности, передайте waitForSelector в тот же payload Quick Actions, чтобы дождаться появления элемента, который появляется только после загрузки контента. Дополнительную информацию см. в Тайм-ауты Quick Actions в Browser Run.
3. Тестирование предварительного рендеринга
-
Запустите Worker в удалённом режиме:
npx wrangler dev --remote.quickAction()метод пока не поддерживается в режиме локальной разработки. Используйтеwrangler dev --remoteпри локальном тестировании Quick Actions Browser Run. -
В другом терминале запросите отрендеренную страницу:
curl "http://localhost:8787/?url=https://example.com/"Ответ должен содержать отрендеренный HTML целевой страницы.
4. Разверните
-
Разверните Worker после локальной проверки:
npx wrangler deploy -
После развёртывания запросите отрендеренную страницу по URL-адресу вашего Worker:
curl "https://<YOUR_WORKER_HOSTNAME>/?url=https://example.com/"
Особенности работы в продакшене
- Рендерите только те хосты, которые вы контролируете
- Используйте Worker в качестве первой точки входа, когда нужна маршрутизация на edge
- Вызывайте Browser Run только для запросов краулеров или интеграций
- Кэшируйте отрендеренный HTML, если ожидаете повторные запросы краулеров
- Обновляйте кешированный HTML при изменении исходного содержимого