---
title: "Generating PDFs in Astro with Cloudflare Browser Rendering at the Edge"
description: "Learn how to generate invoices, reports, and OG images as PDFs directly from your Astro pages using Cloudflare Browser Rendering. Run headless Chromium at the edge, apply print CSS, and stream PDFs with minimal latency."
source: "https://www.launchfa.st/blog/astro-cloudflare-browser-rendering-pdf"
author: "Rishi Raj Jain"
created_at: 2025-12-08T13:00:00.000Z
updated_at: 2026-07-02T00:00:00.000Z
keywords: "astro, cloudflare, browser rendering, pdf, chromium, print css, edge computing"
---

> **Site index for agents**
> Fetch https://www.launchfa.st/llms.txt to discover every page on this site.
> Every page is also available as Markdown by appending `.md` to its URL.

<img width="1600" height="900" alt="Generating PDFs in Astro with Cloudflare Browser Rendering at the Edge" decoding="async" loading="eager" class="mt-4 border rounded-sm bg-cover bg-center bg-no-repeat transform will-change-auto" src="https://ik.imagekit.io/vjeqenuhn/launchfast-website/astro-cloudflare-browser-rendering-pdf.png" />

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.

[Cloudflare Browser Rendering](https://developers.cloudflare.com/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 network 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.

- [View example project on GitHub](https://github.com/rishi-raj-jain/my-astro-kv-pdf-app)

## Prerequisites

You'll need:

- [Node.js 20](https://nodejs.org/en/blog/announcements/v20-release-announce) or later
- A [Cloudflare](https://dash.cloudflare.com/sign-up) account

## 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:

```bash
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:

```bash
npm run dev
```

The app should be running on [localhost:4321](http://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:

```bash
npx astro add cloudflare --yes
```

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

```javascript
// File: 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:

```jsonc
// File: 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:

```bash
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:

```jsonc
// File: 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:

```typescript
// File: 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:

```astro
---
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

```typescript
// File: 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](/blog/implement-basic-authorization-astro) 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)](/blog/rate-limiting-astro-cloudflare-workers) for high-traffic endpoints.

## Deploy to Cloudflare Workers

Deploy your PDF generation application to production:

```bash
# 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](https://twitter.com/direct_messages/create/rishi_raj_jain_).
