Generating PDFs in Astro with Cloudflare Browser Rendering at the Edge
LaunchFast Logo LaunchFast

Generating PDFs in Astro with Cloudflare Browser Rendering at the Edge

Rishi Raj Jain
Generating PDFs in Astro with Cloudflare Browser Rendering at the Edge

Generating PDFs server-side typically means wrestling with Lambda cold starts, managing Chromium binaries, or dealing with third-party APIs that add latency and cost. If you’ve ever spun up a 1GB+ Docker container just to convert HTML to PDF, you know the pain.

High Quality Starter Kits with built-in authentication flow (Auth.js), object uploads (AWS, Clouflare R2, Firebase Storage, Supabase Storage), integrated payments (Stripe, LemonSqueezy), email verification flow (Resend, Postmark, Sendgrid), and much more. Compatible with any database (Redis, Postgres, MongoDB, SQLite, Firestore).
Next.js Starter Kit
SvelteKit Starter Kit

Cloudflare Browser Rendering flips this model. It’s a managed headless Chromium service that runs on Cloudflare’s edge network, i.e. no binaries to bundle, no cold starts, no Docker layers. You get Puppeteer-compatible APIs that execute in a globally distributed with sub-100ms startup times.

In this guide, you’ll build a production-ready PDF generation system using Astro, Cloudflare KV and Cloudflare Workers. You’ll create printable receipt pages with custom CSS, wire up an API endpoint that launches headless Chrome at the edge, and cache the results in Cloudflare KV to avoid redundant rendering.

Prerequisites

You’ll need:

Why Cloudflare Browser Rendering for PDFs?

  • No Chromium babysitting: Cloudflare runs and patches headless Chromium for you.
  • Edge locality: Render PDFs close to the requester; great for dashboards, invoices, and OG images.
  • Streaming: Send PDFs directly from the worker without temp files.
  • Cost-aware: Pay for usage without maintaining a fleet of renderers.

Architecture overview

1. Astro Page (src/pages/receipt/[id].astro)
→ SSR route that renders your receipt/invoice HTML + print CSS
2. PDF API (src/pages/api/pdf/[id].ts)
→ Creates a Browser Rendering session
→ Loads the Astro route URL
→ Calls page.pdf() and streams the binary
3. Cloudflare Adapter + Browser binding
→ Deploys Astro to Workers/Pages
→ Provides a BROWSER binding to the worker

Create a new Astro application

Let’s get started by creating a new Astro project. Execute the following command:

Terminal window
npm create astro@latest my-astro-kv-pdf-app
cd my-astro-kv-pdf-app
npm install wrangler @cloudflare/puppeteer

When prompted, choose:

  • Use minimal (empty) template when prompted on how to start the new project.
  • Yes when prompted to install dependencies.
  • Yes when prompted to initialize a git repository.

Once that’s done, you can move into the project directory and start the app:

Terminal window
npm run dev

The app should be running on localhost:4321.

Integrate Cloudflare adapter in your Astro project

To deploy your Astro project to Cloudflare Workers and use Cloudflare KV, you need to install the Cloudflare adapter. Execute the command below:

Terminal window
npx astro add cloudflare --yes

Update astro.config.mjs to have output as server:

astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server',
adapter: cloudflare()
});

Configure Browser Rendering in wrangler.jsonc

Add a BROWSER binding so the worker can launch headless Chromium:

wrangler.jsonc
{
"main": "dist/_worker.js/index.js",
"name": "my-astro-kv-pdf-app",
"compatibility_date": "2025-12-06",
"compatibility_flags": [
"nodejs_compat",
"global_fetch_strictly_public"
],
"assets": {
"binding": "ASSETS",
"directory": "./dist"
},
"observability": {
"enabled": true
},
"browser": {
"binding": "BROWSER"
}
}

The browser block provisions Cloudflare’s Browser Rendering service and exposes it to your worker as env.BROWSER.

Add KV for caching

Create a KV namespace to cache generated PDFs:

Terminal window
npx wrangler kv namespace create PDF_CACHE

This will output an id that you’ll add to wrangler.jsonc. Update the file to include the KV binding:

wrangler.jsonc
{
"main": "dist/_worker.js/index.js",
"name": "my-astro-kv-pdf-app",
"compatibility_date": "2025-12-06",
"compatibility_flags": [
"nodejs_compat",
"global_fetch_strictly_public"
],
"assets": {
"binding": "ASSETS",
"directory": "./dist"
},
"observability": {
"enabled": true
},
"browser": {
"binding": "BROWSER"
},
"kv_namespaces": [
{
"binding": "PDF_CACHE",
"id": "generated-id",
"remote": true
}
]
}

Add TypeScript types

Create src/env.d.ts to add proper types for the Cloudflare bindings:

src/env.d.ts
/// <reference types="astro/client" />
type KVNamespace = import('@cloudflare/workers-types').KVNamespace
type ENV = {
PDF_CACHE: KVNamespace
BROWSER: Fetcher
}
type Runtime = import('@astrojs/cloudflare').Runtime<ENV>
declare namespace App {
interface Locals extends Runtime {}
}

This gives you full type safety when accessing locals.runtime.env.PDF_CACHE and locals.runtime.env.BROWSER in your routes.

Build a printable Astro page

Create a receipt page with print-friendly styles:

---
const { id } = Astro.params;
const order = {
id,
customer: 'Ada Lovelace',
total: '$128.00',
items: [
{ name: 'Edge Functions', price: '$50.00' },
{ name: 'PDF Rendering', price: '$78.00' }
]
};
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Receipt #{order.id}</title>
<style>
:root { font-family: 'Inter', system-ui, -apple-system, sans-serif; }
body { margin: 0; padding: 32px; color: #0f172a; }
header { display: flex; align-items: center; justify-content: space-between; }
table { width: 100%; border-collapse: collapse; margin-top: 24px; }
th, td { padding: 12px 8px; text-align: left; border-bottom: 1px solid #e2e8f0; }
.total { text-align: right; font-size: 1.125rem; font-weight: 700; }
@page { margin: 24mm; }
@media print {
body { background: white; }
a[href]::after { content: ''; }
}
</style>
</head>
<body>
<header>
<div>
<h1>Receipt #{order.id}</h1>
<p>Issued to {order.customer}</p>
</div>
<strong>launchfa.st</strong>
</header>
<table>
<thead>
<tr><th>Item</th><th>Price</th></tr>
</thead>
<tbody>
{order.items.map(item => (
<tr>
<td>{item.name}</td>
<td>{item.price}</td>
</tr>
))}
</tbody>
</table>
<p class="total">Total: {order.total}</p>
</body>
</html>

Save it as src/pages/receipt/[id].astro. The print CSS (@page) sets margins, and the @media print rules remove noisy link decorations.

Create the PDF API route

Here’s how to create a Worker-style API route that uses Cloudflare’s browser rendering to generate and cache a PDF version of your Astro HTML receipt page.

We’ll set up an API endpoint (src/pages/api/pdf/[id].ts) that:

  • checks if a PDF is already cached in Cloudflare KV and returns it immediately if available
  • launches a headless browser via the Cloudflare Browser Rendering API if not cached
  • navigates to the printable page and generates a PDF
  • stores the resulting PDF in KV for 7 days for future requests
src/pages/api/pdf/[id].ts
import type { APIRoute } from "astro";
import puppeteer from "@cloudflare/puppeteer";
// Helper to create a proper PDF HTTP response
const pdfResponse = (pdf: Buffer<ArrayBuffer>, id: string) => {
return new Response(pdf, {
headers: {
'Content-Type': 'application/pdf',
'CDN-Cache-Control': 'max-age=604800',
'Content-Disposition': `inline; filename="receipt-${id}.pdf"`
}
});
}
export const GET: APIRoute = async ({ locals, params, request }) => {
const id = params.id as string;
const origin = new URL(request.url).origin;
const targetUrl = `${origin}/receipt/${id}`;
const cache = locals.runtime.env.PDF_CACHE;
// Try KV cache first to avoid unnecessary browser work
const cachedPdf = await cache.get(targetUrl, "arrayBuffer");
if (cachedPdf)
return pdfResponse(Buffer.from(cachedPdf), id);
// Only run browser in Cloudflare's env
if (!locals.runtime.env.BROWSER)
return new Response('Browser binding not available locally', { status: 501 });
// Launch browser and render HTML to PDF
const session = await puppeteer.launch(locals.runtime.env.BROWSER);
const page = await session.newPage();
await page.goto(targetUrl, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '24mm', right: '18mm', bottom: '24mm', left: '18mm' }
});
await session.close();
const result = Buffer.from(pdf);
// Cache for a week (604800 seconds)
await cache.put(targetUrl, result, { expirationTtl: 60 * 60 * 24 * 7 });
return pdfResponse(result, id);
};

Now, when you hit /api/pdf/123, you’ll receive a PDF generated live from /receipt/123. Subsequent requests return the cached PDF until it expires.

Protect and cache your PDFs

  • Auth: Require a session token or signed query string before rendering. Reject unauthenticated requests early.
  • Caching: Cache static invoices for short periods (Cache-Control: public, max-age=300) to avoid re-rendering. For dynamic content, keep cache private.
  • Rate limits: Add a rate limiter (KV) for high-traffic endpoints.

Deploy to Cloudflare Workers

Deploy your PDF generation application to production:

Terminal window
# Build the project
npm run build
# Deploy to Cloudflare Workers
npx wrangler deploy

After deployment, your API will be live at https://your-worker.workers.dev/api/pdf/[id].

Conclusion

Cloudflare Browser Rendering transforms PDF generation from a resource-heavy operation into a lightweight edge function. By combining Astro’s SSR capabilities with headless Chromium at the edge, you can generate invoices, receipts, and reports on-demand without managing infrastructure or dealing with cold starts.

For advanced use cases, explore multi-page documents, custom headers/footers, or even screenshot generation for Open Graph images. The Browser Rendering API gives you full Puppeteer control, all running at Cloudflare’s edge.

If you have any questions or comments, feel free to reach out to me on Twitter.

Learn More Implement Rate Limiting in Astro with Cloudflare Workers
Implement Rate Limiting in Astro with Cloudflare Workers December 6, 2025
Setting Up Privacy-Focused Analytics with Tinybird in Astro
Setting Up Privacy-Focused Analytics with Tinybird in Astro December 5, 2025
Building Real-Time Chat in Astro with Cloudflare Durable Objects and WebSocket Hibernation
Building Real-Time Chat in Astro with Cloudflare Durable Objects and WebSocket Hibernation December 4, 2025