-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[Browser Rendering] Guide for PDF Rendering from HTML and CSS #17510
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| --- | ||
| title: How To | ||
| pcx_content_type: navigation | ||
| sidebar: | ||
| order: 4 | ||
| group: | ||
| hideIndex: true | ||
| --- | ||
|
|
||
| import { DirectoryListing } from "~/components"; | ||
|
|
||
| <DirectoryListing /> |
267 changes: 267 additions & 0 deletions
267
src/content/docs/browser-rendering/how-to/pdf-generation.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,267 @@ | ||
| --- | ||
| pcx_content_type: how-to | ||
| title: Generate PDFs Using HTML and CSS | ||
| sidebar: | ||
| order: 1 | ||
| --- | ||
|
|
||
| import { Aside } from "~/components"; | ||
|
|
||
| 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. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| 1. Use the `create-cloudflare` CLI to generate a new Hello World Cloudflare Worker script: | ||
|
|
||
| ```sh | ||
| npm create cloudflare@latest -- browser-worker | ||
| ``` | ||
|
|
||
| 2. Install `@cloudflare/puppeteer`, allowing us to control the Browser Rendering instance: | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| ```sh | ||
| npm install @cloudflare/puppeteer --save-dev | ||
| ``` | ||
|
|
||
| 3. Add our Browser Rendering binding to our new `wrangler.toml` configuration: | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| ```yaml | ||
| browser = { binding = "BROWSER" } | ||
| ``` | ||
|
|
||
| 4. Replace the contents of `src/index.ts` (or `src/index.js` for JavaScript projects) with the following skeleton script: | ||
|
|
||
| ```ts | ||
| import puppeteer from "@cloudflare/puppeteer"; | ||
|
|
||
| const generateDocument = (name: string) => {}; | ||
|
|
||
| export default { | ||
| async fetch(request, env) { | ||
| const { searchParams } = new URL(request.url); | ||
| let name = searchParams.get("name"); | ||
|
|
||
| if (!name) { | ||
| return new Response("Please provide a name using the ?name= parameter"); | ||
| } | ||
|
|
||
| const browser = await puppeteer.launch(env.BROWSER); | ||
| const page = await browser.newPage(); | ||
|
|
||
| // Step 1: Define HTML and CSS | ||
| const document = generateDocument(name); | ||
|
|
||
| // Step 2: Send HTML and CSS to our browser | ||
| await page.setContent(document); | ||
|
|
||
| // Step 3: Generate and return PDF | ||
|
|
||
| return new Response(); | ||
| }, | ||
| }; | ||
| ``` | ||
|
|
||
| ## Step One: Define HTML and CSS | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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. | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| <Aside> | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| It’s worth noting that you can generate your HTML or CSS using any method | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| you’d like. For now we’re using string interpolation, but this method is | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| fully-compatible with web frameworks capable of rendering HTML on Workers such | ||
| as React, Remix, and Vue. | ||
| </Aside> | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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. | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| To start, let’s fill out our `generateDocument` function with the following: | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| ```ts | ||
| const generateDocument = (name: string) => { | ||
| return ` | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <style> | ||
| html, | ||
| body, | ||
| #container { | ||
| width: 100%; | ||
| height: 100%; | ||
| margin: 0; | ||
| } | ||
| body { | ||
| font-family: Baskerville, Georgia, Times, serif; | ||
| background-color: #f7f1dc; | ||
| } | ||
| strong { | ||
| color: #5c594f; | ||
| font-size: 128px; | ||
| margin: 32px 0 48px 0; | ||
| } | ||
| em { | ||
| font-size: 24px; | ||
| } | ||
| #container { | ||
| flex-direction: column; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| text-align: center; | ||
| } | ||
| </style> | ||
| </head> | ||
|
|
||
| <body> | ||
| <div id="container"> | ||
| <em>This is to certify that</em> | ||
| <strong>${name}</strong> | ||
| <em>has rendered a PDF using Cloudflare Workers</em> | ||
| </div> | ||
| </body> | ||
| </html> | ||
| `; | ||
| }; | ||
| ``` | ||
|
|
||
| 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. | ||
|
|
||
| <Aside> | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| It’s usually best to avoid directly interpolating user-provided content into | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| an image or PDF renderer in production applications. To render contents like | ||
| an invoice, it wold be best to validate the data input, and fetch data | ||
| yourself using tools like [D1](https://developers.cloudflare.com/d1) or | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| [Workers KV](https://developers.cloudflare.com/workers-kv). | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| </Aside> | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| ## Step Two: Load HTML and CSS Into Browser | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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: | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| ```ts | ||
| const browser = await puppeteer.launch(env.BROWSER); | ||
| const page = await browser.newPage(); | ||
| ``` | ||
|
|
||
| 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: | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| ```ts | ||
| await page.setContent(document); | ||
| ``` | ||
|
|
||
| ## Step Three: Generate and Return PDF | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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. | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| ```ts | ||
| let pdf = page.pdf({ printBackground: true }); | ||
| ``` | ||
|
|
||
| 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. | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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: | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| ```ts | ||
| return new Response(pdf, { | ||
| headers: { | ||
| "content-type": "application/pdf", | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| ## Conclusion | ||
|
|
||
| The full Worker script now looks as follows: | ||
|
|
||
| ```ts | ||
| import puppeteer from "@cloudflare/puppeteer"; | ||
|
|
||
| const generateDocument = (name: string) => { | ||
| return ` | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <style> | ||
| html, body, #container { | ||
| width: 100%; | ||
| height: 100%; | ||
| margin: 0; | ||
| } | ||
| body { | ||
| font-family: Baskerville, Georgia, Times, serif; | ||
| background-color: #f7f1dc; | ||
| } | ||
| strong { | ||
| color: #5c594f; | ||
| font-size: 128px; | ||
| margin: 32px 0 48px 0; | ||
| } | ||
| em { | ||
| font-size: 24px; | ||
| } | ||
| #container { | ||
| flex-direction: column; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| text-align: center | ||
| } | ||
| </style> | ||
| </head> | ||
|
|
||
| <body> | ||
| <div id="container"> | ||
| <em>This is to certify that</em> | ||
| <strong>${name}</strong> | ||
| <em>has rendered a PDF using Cloudflare Workers</em> | ||
| </div> | ||
| </body> | ||
| </html> | ||
| `; | ||
| }; | ||
|
|
||
| export default { | ||
| async fetch(request, env) { | ||
| const { searchParams } = new URL(request.url); | ||
| let name = searchParams.get("name"); | ||
|
|
||
| if (!name) { | ||
| return new Response("Please provide a name using the ?name= parameter"); | ||
| } | ||
|
|
||
| const browser = await puppeteer.launch(env.BROWSER); | ||
| const page = await browser.newPage(); | ||
|
|
||
| // Step 1: Define HTML and CSS | ||
| const document = generateDocument(name); | ||
|
|
||
| // // Step 2: Send HTML and CSS to our browser | ||
| await page.setContent(document); | ||
|
|
||
| // // Step 3: Generate and return PDF | ||
| const pdf = await page.pdf({ printBackground: true }); | ||
|
|
||
| return new Response(pdf, { | ||
| headers: { | ||
| "content-type": "application/pdf", | ||
| }, | ||
| }); | ||
| }, | ||
| }; | ||
| ``` | ||
|
|
||
| We can run this script to test it using Wrangler’s `--remote` flag: | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| ```sh | ||
| npx wrangler@latest dev --remote | ||
| ``` | ||
|
|
||
| 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: | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| . | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| --- | ||
|
|
||
| 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). | ||
Oxyjun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.