Skip to content

Commit ad3dbec

Browse files
committed
Create guide for PDF rendering using workers
1 parent c2067ef commit ad3dbec

File tree

3 files changed

+279
-0
lines changed

3 files changed

+279
-0
lines changed
103 KB
Loading
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
title: How To
3+
pcx_content_type: navigation
4+
sidebar:
5+
order: 4
6+
group:
7+
hideIndex: true
8+
---
9+
10+
import { DirectoryListing } from "~/components";
11+
12+
<DirectoryListing />
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
---
2+
pcx_content_type: how-to
3+
title: Generate PDFs Using HTML and CSS
4+
sidebar:
5+
order: 1
6+
---
7+
8+
import { Aside } from "~/components";
9+
10+
As seen in the [Getting Started guide](https://developers.cloudflare.com/browser-rendering/get-started/screenshots/), Browser Rendering can be used to generate screenshots for any given URL. Alongside screenshots, we can also generate full PDF documents for a given webpage, and can also provide the webpage markup and style ourselves.
11+
12+
## Prerequisites
13+
14+
1. Use the `create-cloudflare` CLI to generate a new Hello World Cloudflare Worker script:
15+
16+
```sh
17+
npm create cloudflare@latest -- browser-worker
18+
```
19+
20+
2. Install `@cloudflare/puppeteer`, allowing us to control the Browser Rendering instance:
21+
22+
```sh
23+
npm install @cloudflare/puppeteer --save-dev
24+
```
25+
26+
3. Add our Browser Rendering binding to our new `wrangler.toml` configuration:
27+
28+
```yaml
29+
browser = { binding = "BROWSER" }
30+
```
31+
32+
4. Replace the contents of `src/index.ts` (or `src/index.js` for JavaScript projects) with the following skeleton script:
33+
34+
```ts
35+
import puppeteer from "@cloudflare/puppeteer";
36+
37+
const generateDocument = (name: string) => {};
38+
39+
export default {
40+
async fetch(request, env) {
41+
const { searchParams } = new URL(request.url);
42+
let name = searchParams.get("name");
43+
44+
if (!name) {
45+
return new Response("Please provide a name using the ?name= parameter");
46+
}
47+
48+
const browser = await puppeteer.launch(env.BROWSER);
49+
const page = await browser.newPage();
50+
51+
// Step 1: Define HTML and CSS
52+
const document = generateDocument(name);
53+
54+
// Step 2: Send HTML and CSS to our browser
55+
await page.setContent(document);
56+
57+
// Step 3: Generate and return PDF
58+
59+
return new Response();
60+
},
61+
};
62+
```
63+
64+
## Step One: Define HTML and CSS
65+
66+
Rather than using Browser Rendering to navigate to a user-provided URL, here we’re going to generate a webpage manually and then provide that webpage to the Browser Rendering instance, allowing us to render any design we please.
67+
68+
<Aside>
69+
It’s worth noting that you can generate your HTML or CSS using any method
70+
you’d like. For now we’re using string interpolation, but this method is
71+
fully-compatible with web frameworks capable of rendering HTML on Workers such
72+
as React, Remix, and Vue.
73+
</Aside>
74+
75+
For this example, we’re going to take in user-provided content (via a `?name=` parameter), and have that name output in the final PDF document.
76+
77+
To start, let’s fill out our `generateDocument` function with the following:
78+
79+
```ts
80+
const generateDocument = (name: string) => {
81+
return `
82+
<!DOCTYPE html>
83+
<html lang="en">
84+
<head>
85+
<meta charset="utf-8" />
86+
<style>
87+
html,
88+
body,
89+
#container {
90+
width: 100%;
91+
height: 100%;
92+
margin: 0;
93+
}
94+
body {
95+
font-family: Baskerville, Georgia, Times, serif;
96+
background-color: #f7f1dc;
97+
}
98+
strong {
99+
color: #5c594f;
100+
font-size: 128px;
101+
margin: 32px 0 48px 0;
102+
}
103+
em {
104+
font-size: 24px;
105+
}
106+
#container {
107+
flex-direction: column;
108+
display: flex;
109+
align-items: center;
110+
justify-content: center;
111+
text-align: center;
112+
}
113+
</style>
114+
</head>
115+
116+
<body>
117+
<div id="container">
118+
<em>This is to certify that</em>
119+
<strong>${name}</strong>
120+
<em>has rendered a PDF using Cloudflare Workers</em>
121+
</div>
122+
</body>
123+
</html>
124+
`;
125+
};
126+
```
127+
128+
This example HTML document should render a beige background imitating a certificate showing that the user-provided name has successfully rendered a PDF using Cloudflare Workers.
129+
130+
<Aside>
131+
It’s usually best to avoid directly interpolating user-provided content into
132+
an image or PDF renderer in production applications. To render contents like
133+
an invoice, it wold be best to validate the data input, and fetch data
134+
yourself using tools like [D1](https://developers.cloudflare.com/d1) or
135+
[Workers KV](https://developers.cloudflare.com/workers-kv).
136+
</Aside>
137+
138+
## Step Two: Load HTML and CSS Into Browser
139+
140+
Now that we have our fully-styled HTML document, we can take the contents and send it to our browser instance. We can create an empty page to store this document as follows:
141+
142+
```ts
143+
const browser = await puppeteer.launch(env.BROWSER);
144+
const page = await browser.newPage();
145+
```
146+
147+
The [`page.setContent()`](https://github.com/cloudflare/puppeteer/blob/main/docs/api/puppeteer.page.setcontent.md) function can then be used to set the page’s HTML contents from a string, so we pass in our created document directly like so:
148+
149+
```ts
150+
await page.setContent(document);
151+
```
152+
153+
## Step Three: Generate and Return PDF
154+
155+
With our Browser Rendering instance now rendering our provided HTML and CSS, we can use the [`page.pdf()`](https://github.com/cloudflare/puppeteer/blob/main/docs/api/puppeteer.page.pdf.md) command to generate a PDF file and return it to the client.
156+
157+
```ts
158+
let pdf = page.pdf({ printBackground: true });
159+
```
160+
161+
The `page.pdf()` call supports a [number of options](https://github.com/cloudflare/puppeteer/blob/main/docs/api/puppeteer.pdfoptions.md), including setting the dimensions of the generated PDF to a specific paper size, setting specific margins, and allowing fully-transparent backgrounds. For now, we’re only overriding the `printBackground` option to allow our `body` background styles to show up.
162+
163+
Now that we have our PDF data, it’s just a matter of returning it to the client in the `Response` with an `application/pdf` content type:
164+
165+
```ts
166+
return new Response(pdf, {
167+
headers: {
168+
"content-type": "application/pdf",
169+
},
170+
});
171+
```
172+
173+
## Conclusion
174+
175+
The full Worker script now looks as follows:
176+
177+
```ts
178+
import puppeteer from "@cloudflare/puppeteer";
179+
180+
const generateDocument = (name: string) => {
181+
return `
182+
<!DOCTYPE html>
183+
<html lang="en">
184+
<head>
185+
<meta charset="utf-8" />
186+
<style>
187+
html, body, #container {
188+
width: 100%;
189+
height: 100%;
190+
margin: 0;
191+
}
192+
body {
193+
font-family: Baskerville, Georgia, Times, serif;
194+
background-color: #f7f1dc;
195+
}
196+
strong {
197+
color: #5c594f;
198+
font-size: 128px;
199+
margin: 32px 0 48px 0;
200+
}
201+
em {
202+
font-size: 24px;
203+
}
204+
#container {
205+
flex-direction: column;
206+
display: flex;
207+
align-items: center;
208+
justify-content: center;
209+
text-align: center
210+
}
211+
</style>
212+
</head>
213+
214+
<body>
215+
<div id="container">
216+
<em>This is to certify that</em>
217+
<strong>${name}</strong>
218+
<em>has rendered a PDF using Cloudflare Workers</em>
219+
</div>
220+
</body>
221+
</html>
222+
`;
223+
};
224+
225+
export default {
226+
async fetch(request, env) {
227+
const { searchParams } = new URL(request.url);
228+
let name = searchParams.get("name");
229+
230+
if (!name) {
231+
return new Response("Please provide a name using the ?name= parameter");
232+
}
233+
234+
const browser = await puppeteer.launch(env.BROWSER);
235+
const page = await browser.newPage();
236+
237+
// Step 1: Define HTML and CSS
238+
const document = generateDocument(name);
239+
240+
// // Step 2: Send HTML and CSS to our browser
241+
await page.setContent(document);
242+
243+
// // Step 3: Generate and return PDF
244+
const pdf = await page.pdf({ printBackground: true });
245+
246+
return new Response(pdf, {
247+
headers: {
248+
"content-type": "application/pdf",
249+
},
250+
});
251+
},
252+
};
253+
```
254+
255+
We can run this script to test it using Wrangler’s `--remote` flag:
256+
257+
```sh
258+
npx wrangler@latest dev --remote
259+
```
260+
261+
With our script now running, we can pass in a `?name` parameter to the local URL (such as `http://localhost:8787/?name=Harley`) and we should see the following:
262+
263+
![A screenshot of a generated PDF, with the author’s name shown in a mock certificate.](~/assets/images/browser-rendering/pdf-generation.png).
264+
265+
---
266+
267+
Dynamically generating PDF documents solves a number of common use-cases, from invoicing customers to archiving documents to creating dynamic certificates (as seen in our simple example here).

0 commit comments

Comments
 (0)