Skip to content

Browser

Source: src/Cloudflare/Browser/Browser.ts

A Cloudflare Browser Rendering binding for launching headless browser sessions from Workers via @cloudflare/puppeteer.

Yielding the marker attaches the binding to the surrounding Worker and returns the runtime {@link BrowserClient}. Every cf.BrowserRun method is mirrored as an Effect, so no Effect.tryPromise wrapping is needed.

import * as Effect from "effect/Effect";
Cloudflare.Worker(
"BrowserWorker",
{ main: import.meta.filename },
Effect.gen(function* () {
const browser = yield* Cloudflare.Browser({ name: "BROWSER" });
return {
fetch: Effect.gen(function* () {
return yield* browser.markdown({ url: "https://example.com" });
}),
};
}).pipe(Effect.provide(Cloudflare.BrowserBindingLive)),
);

Render content, screenshot, PDF, and structured data

JSON quick actions resolve to their parsed payload; binary actions (screenshot, pdf) resolve to a Stream of bytes. No Promise or Response.json() in sight.

import * as Effect from "effect/Effect";
import * as Stream from "effect/Stream";
const browser = yield* Cloudflare.Browser({ name: "BROWSER" });
const url = "https://example.com";
// HTML content — parsed payload, title lives in `meta`.
const content = yield* browser.content({ url });
const title = content.meta.title;
// Scrape elements by CSS selector.
yield* browser.scrape({ url, elements: [{ selector: "h1" }] });
// Extract all links.
const { result: links } = yield* browser.links({ url });
// Binary actions stream bytes — collect or pipe them.
const png = yield* browser.screenshot({ url }).pipe(Stream.runCollect);
const pdf = yield* browser.pdf({ url }).pipe(Stream.runCollect);
// AI-extracted structured data.
yield* browser.json({ url, prompt: "Extract the page heading" });

Call the generic quickAction directly

The named methods are thin wrappers over quickAction, which mirrors cf.BrowserRun["quickAction"] one-to-one.

const browser = yield* Cloudflare.Browser({ name: "BROWSER" });
const res = yield* browser.quickAction("snapshot", {
url: "https://example.com",
});

raw hands you the underlying runtime binding. Puppeteer is promise-based, so this is the one place you reach for Effect.tryPromise.

import puppeteer from "@cloudflare/puppeteer";
import * as Effect from "effect/Effect";
Cloudflare.Worker(
"BrowserWorker",
{ main: import.meta.filename },
Effect.gen(function* () {
const browser = yield* Cloudflare.Browser({ name: "BROWSER" });
return {
fetch: Effect.gen(function* () {
const binding = yield* browser.raw;
const session = yield* Effect.tryPromise(() =>
puppeteer.launch(binding),
);
try {
const page = yield* Effect.tryPromise(() => session.newPage());
yield* Effect.tryPromise(() => page.goto("https://example.com"));
const title = yield* Effect.tryPromise(() => page.title());
return Response.json({ title });
} finally {
yield* Effect.promise(() => session.close());
}
}),
};
}).pipe(Effect.provide(Cloudflare.BrowserBindingLive)),
);

Declare the binding on env

export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
env: {
BROWSER: Cloudflare.Browser(),
},
});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
// { BROWSER: BrowserRun }

Async-style worker with the raw runtime binding

import puppeteer from "@cloudflare/puppeteer";
import type { WorkerEnv } from "../alchemy.run.ts";
export default {
async fetch(request: Request, env: WorkerEnv) {
const browser = await puppeteer.launch(env.BROWSER);
const page = await browser.newPage();
await page.goto("https://example.com");
const screenshot = await page.screenshot();
await browser.close();
return new Response(screenshot, {
headers: { "content-type": "image/png" },
});
},
};