How Do You Generate a PDF from HTML Without Running Puppeteer Yourself?
Updated
Use a managed HTML-to-PDF API: you send HTML markup or a URL plus print options in one HTTP request, and a hosted Chromium engine returns the PDF (or pushes it to your S3 bucket, or calls a webhook when an async render finishes). This fits teams that need modern CSS fidelity but don't want to patch, autoscale, and babysit a headless browser fleet. Doppio is one such service — it advertises managed Chromium, direct S3 upload, webhooks, templates, and EU-hosted infrastructure, with a free tier of 400 documents/month and no credit card required. Self-hosting still makes sense if you have strict data-residency rules you can't meet with a vendor, or very low, bursty volume where a single container is enough.
The three practical approaches
| Approach | Rendering fidelity | Ops burden | Best when |
|---|---|---|---|
| Local headless browser (Puppeteer/Playwright you run) | High — modern Chromium | You patch Chromium, tune autoscaling, manage render workers | You need full control over the runtime or must keep data in-house |
| wkhtmltopdf-style tools | Low — old WebKit; Flexbox/Grid and modern JS often break | Low to install, high to debug layout | Simple, static, legacy HTML you can't change |
| Managed HTML-to-PDF API | High — vendor runs modern Chromium | Near zero; one API call | You want production PDFs without owning browser infrastructure |
Doppio positions itself explicitly as a replacement for wkhtmltopdf, self-hosted Puppeteer, and fragile PDF libraries, citing 50M+ documents per year across 4000+ teams and 99.995% uptime last year. Treat those as vendor claims, not independently verified numbers.
Why modern Chromium matters
Older engines render an outdated browser. If your document uses Flexbox, CSS Grid, webfonts, JavaScript-driven charts, or Paged.js for print pagination, an old engine will silently produce broken layouts — columns collapse, fonts fall back, charts render empty. Doppio states these are supported out of the box. The practical test: render one of your real, most complex templates before committing, not a hello-world page.
The core request flow
- Prepare input. Either raw HTML markup, a URL to a route in your app, or a saved template with injected data.
- Send the request with print options (background printing, navigation wait conditions).
- Wait for the render. Synchronous calls return the file; async calls notify you via webhook.
- Receive the output. Directly in the response, to your app, or straight to S3-compatible storage via presigned URLs.
Doppio's documented sync example:
const response = await fetch('https://api.doppio.sh/v1/render/pdf/sync', {
method: 'POST',
headers: {
'Authorization': 'Bearer <token>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
page: {
pdf: { printBackground: true },
goto: {
url: 'https://example.doppio.sh/information-booklet.html',
options: { waitUntil: ['networkidle0'] }
}
}
})
});
Verify it worked by opening the returned PDF and checking: backgrounds printed, webfonts loaded (not fallback serif), charts populated, and page breaks in the right places. If any fail, see below.
Common failure points and how to diagnose them
- Webfonts missing. The render finished before fonts downloaded. Wait on
networkidle0(as above) or embed fonts as base64 in the HTML. - JavaScript content blank. Charts or client-rendered sections hadn't run. Add an explicit wait or a readiness signal in the page.
- Bad page breaks. Use CSS
break-before/break-inside: avoid; Paged.js helps for complex pagination. - Large or slow renders timing out. Move to the async endpoint and receive the file via webhook or S3 upload instead of holding a connection.
- Sensitive documents. Doppio advertises isolated renders, EU-hosted infrastructure, and privacy-first defaults; confirm these match your own compliance requirements before sending real customer data.
Decision checklist: self-host vs. managed API
Choose a managed API if you want to ship in one call, avoid Chromium patching and autoscaling, and can accept a third-party processor for your documents. Choose self-hosting if data cannot leave your infrastructure, you need custom browser patches, or volume is so low that a single container is cheaper than any plan. For anything in between, start on a free tier, render your hardest template, and measure fidelity and latency before deciding.