How to Render HTML with Puppeteer
Render an HTML string or URL with Puppeteer, then export it as a PDF or screenshot. Learn how to wait for page readiness, tune output, and handle common failures.
To render an HTML string with Puppeteer, call page.setContent(html); to render a hosted page, call page.goto(url). Then export a PDF with page.pdf() or an image with page.screenshot(). The right readiness wait and output options depend on what the page loads and whether you need print or screen styling.
This guide covers both input paths, complete Node.js examples, PDF and screenshot settings, readiness, troubleshooting, and a hosted screenshot option when you do not want to run a browser yourself.
1. Choose how to provide the HTML
| Input | Puppeteer method | Use it when |
|---|---|---|
| HTML markup string | page.setContent(html, options) |
Your application generated the markup, or you have a local template or fragment. |
| Website URL | page.goto(url, options) |
The page is hosted and should load its normal resources and scripts. |
setContent() assigns markup to the page. goto() navigates to a URL. They are different input methods: use the former for markup you already hold, and the latter for a reachable page. Puppeteer documents both in its setContent API and its screenshot guide.
2. Install Puppeteer and render an HTML string
In a new Node.js project, install Puppeteer with npm:
npm install puppeteer
Save this as render-html.mjs. It sets markup, waits for the document load event, and writes both a screenshot and a PDF:
import puppeteer from 'puppeteer';
const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rendered report</title>
<style>
body { font: 16px/1.5 system-ui, sans-serif; margin: 2rem; color: #172033; }
h1 { color: #175cd3; }
@page { size: A4; margin: 16mm; }
</style>
</head>
<body>
<h1>Monthly report</h1>
<p>This page was rendered from an HTML string.</p>
</body>
</html>`;
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 });
await page.setContent(html, { waitUntil: 'load' });
await page.screenshot({ path: 'rendered.png', fullPage: true });
await page.pdf({ path: 'rendered.pdf', format: 'A4', printBackground: true });
} finally {
await browser.close();
}
The example uses setContent() for markup and page.pdf() and page.screenshot() for output. Use a try/finally so the browser closes even if rendering or file output throws. Puppeteer’s PDF guide notes that PDF generation waits for fonts by default.
3. Render a page from a URL
For a page already served over HTTP or HTTPS, navigate to its URL before exporting. This runnable example creates a full-page PNG:
import puppeteer from 'puppeteer';
const url = process.argv[2] ?? 'https://news.ycombinator.com';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setViewport({ width: 1365, height: 900, deviceScaleFactor: 1 });
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
Run it with node render-url.mjs https://example.com. Puppeteer’s guide uses networkidle2 in its URL screenshot example, but no network-idle condition proves that every application has finished its own client-side work. If a page has a known ready element, wait for that explicitly after navigation.
4. Wait for the page state you need
Waiting is often the difference between a complete capture and a screenshot taken too early. Pick the condition that matches the page:
| Wait strategy | What it is useful for | Limit |
|---|---|---|
load |
Markup and load-event resources have completed. This is the documented default for setContent(). |
Client-side rendering or later requests may still be running. |
domcontentloaded |
The initial document has been parsed and its DOM is available. | Images, fonts, and application data may not be ready. |
networkidle0 / networkidle2 |
A page whose relevant resources settle after network activity. | Persistent connections, analytics, polling, or app behavior can make this unsuitable or misleading. |
| Selector wait | A specific component or rendered result must exist. | Existence does not necessarily mean its content or animation is finished. |
| Explicit delay | A known short animation or delayed update needs time to complete. | Fixed sleeps can waste time or remain too short when conditions vary. |
Example: navigate until the DOM is parsed, then wait for a report element before capturing:
await page.goto('https://example.com/report', {
waitUntil: 'domcontentloaded',
timeout: 60000
});
await page.waitForSelector('[data-render-state="ready"]', { timeout: 15000 });
await page.screenshot({ path: 'report.png', fullPage: true });
For HTML that loads external images, stylesheets, or fonts, ensure those resources are available to the browser. For client-rendered markup, wait for an application-specific signal such as a ready selector rather than assuming navigation completion means the UI is final. Avoid using an arbitrary long delay as a substitute for knowing the page’s readiness condition.
5. Choose PDF or image output
PDF output
page.pdf(options) creates a paginated PDF. Puppeteer uses print CSS media by default. If the page is styled for screen and you want those styles in the PDF, set the media type first:
await page.emulateMediaType('screen');
await page.pdf({ path: 'screen-styled.pdf', format: 'A4', printBackground: true });
Relevant PDF options include:
| Option | Purpose |
|---|---|
format |
Choose a paper preset such as A4 or Letter. |
width, height |
Set custom page dimensions instead of relying on a preset. |
margin |
Set top, right, bottom, and left margins. |
landscape |
Use landscape page orientation. |
scale |
Scale rendered content to fit the page. |
printBackground |
Include background colors and images; printing backgrounds is off unless enabled. |
preferCSSPageSize |
Prefer dimensions declared by CSS @page. |
pageRanges |
Limit output to selected pages. |
displayHeaderFooter, headerTemplate, footerTemplate |
Add print headers or footers when required. |
Example with explicit margins, background printing, and a page range:
await page.pdf({
path: 'selected-pages.pdf',
format: 'A4',
landscape: false,
margin: { top: '16mm', right: '14mm', bottom: '16mm', left: '14mm' },
printBackground: true,
preferCSSPageSize: true,
pageRanges: '1-3'
});
Use CSS print rules and @page when the layout itself must change for paper. To preserve exact print colors where appropriate, Puppeteer’s Page API points to the CSS property -webkit-print-color-adjust. Confirm the installed Puppeteer version’s API documentation for option details.
Screenshot output
page.screenshot(options) captures the rendered page as an image. Set the viewport before navigation or content assignment if layout depends on screen dimensions. Common options include:
| Option | Purpose |
|---|---|
path |
Write the image to a file; without a path the method returns image data. |
type |
Select an available image format such as PNG, JPEG, or WebP according to the installed Puppeteer version. |
quality |
Set lossy image quality for applicable formats. |
fullPage |
Capture the full page content rather than just the viewport. |
clip |
Capture a specific rectangular region. |
omitBackground |
Use a transparent background where supported, such as with PNG. |
To capture one element instead of the full page, select it and call screenshot on its element handle:
const card = await page.waitForSelector('.report-card');
if (!card) throw new Error('Report card was not found');
await card.screenshot({ path: 'report-card.png' });
Puppeteer also documents element screenshots in its screenshot guide. Use fullPage: true when the entire document should be included; use a clip or element handle when you only need a region. Large full-page captures can require substantially more memory than viewport captures.
6. Render HTML with cURL, Python, or Node.js through ScreenshotNeo
Puppeteer runs a browser process that you manage. If the deliverable is a website screenshot or PDF and you want an API call instead, ScreenshotNeo accepts a URL and returns an image or PDF. Its options include HTML/CSS-to-image capture, custom CSS and JavaScript, viewport and device presets, element capture, full-page capture with lazy images loaded, and PDF page and margin settings. See the ScreenshotNeo API documentation for request options.
cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`ScreenshotNeo request failed: ${res.status} ${await res.text()}`);
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for MCP clients including Claude and Cursor. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots. Every feature is available on every plan.
Sign up for 1,000 free screenshots a month, with no card required.
7. Performance, reliability, and cost
- Reuse browser processes for batches. Launching a browser for every item adds startup overhead. Keep one browser alive for a bounded batch, create a fresh page per job, and close pages when done.
- Set timeouts and cleanup. Navigation and selector waits should have limits. Put
browser.close()in afinallyblock so failures do not leave browser processes running. - Control memory. Full-page images, high device scale factors, and multiple concurrent pages increase memory use. Start with viewport captures and limited concurrency, then raise limits based on your own workload.
- Keep outputs deterministic. Fix the viewport, device scale factor, locale-sensitive content where possible, wait condition, and capture time. Dynamic ads, timestamps, remote assets, animations, and personalized content can change output between runs.
- Handle unstable pages explicitly. Retry only transient navigation or network failures with a small bounded retry policy. Do not retry indefinitely, and distinguish a legitimate blank page from a failed load in your own job result.
- Account for infrastructure costs. Self-hosted Puppeteer consumes CPU, memory, disk, and maintenance time for browser installation and updates. The research sources establish no Puppeteer service price or performance benchmark; measure your own pages and deployment environment.
- For API capture costs, check billing signals. ScreenshotNeo bills clean shots only and returns
X-Page-VerdictandX-Billedheaders. Published plans are Free: 1,000/month; Starter: $5 for 3,000; Growth: $15 for 15,000; Pro: $39 for 60,000; Scale: $99 for 250,000; Business: $249 for 1,000,000. Yearly billing gives two months free.
8. Troubleshooting common Puppeteer rendering failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Screenshot is blank or missing content | Capture ran before client-side rendering finished, or a resource failed to load. | Wait for a page-specific ready selector; inspect console and request failures; confirm remote assets are reachable. |
Navigation timeout |
The page keeps requests open, is slow, or the timeout is too short. | Choose a more suitable waitUntil condition, set a finite longer timeout where justified, and wait for the specific content separately. |
setContent() output lacks styles or images |
Markup references relative resources without a base URL, or external assets cannot load. | Use absolute resource URLs or include required CSS/assets inline; verify resource access from the browser environment. |
| PDF ignores screen styling | PDF uses print media by default. | Call await page.emulateMediaType('screen') before page.pdf(), or create print-specific CSS. |
| PDF backgrounds are absent | Background printing is not enabled. | Set printBackground: true. |
| Fonts look wrong or shift after capture | Font resources are missing, blocked, or not ready. | Check font requests and use a readiness condition suited to the page. PDF generation waits for fonts by default, but the font still needs to load successfully. |
| Element screenshot throws or captures the wrong area | The selector did not match, matched an unexpected element, or the element is outside the expected state. | Wait for the selector, validate it identifies the intended element, then capture its handle. |
| Output is unexpectedly huge | Full-page dimensions, device scale factor, or image type/quality drive pixel count and file size. | Reduce viewport or device scale factor, capture a specific region, or choose a suitable lossy format and quality. |
| Browser process remains after an error | Cleanup did not run after an exception. | Close the browser in finally, and close per-job pages when sharing a browser. |
9. FAQ
Can Puppeteer render an HTML string without hosting it?
Yes. Use page.setContent() with the markup string. Referenced assets still need to be available to the browser.
Can Puppeteer turn HTML into a PDF?
Yes. Use page.pdf(). It uses print media by default, so set screen media explicitly if that is the intended appearance.
How do I capture only one part of a page?
Use an element handle’s screenshot() method or the page screenshot’s clip option.
Does network idle mean a single-page app is finished?
No universal wait condition guarantees application readiness. Wait for a signal that represents the content you need.
Where can I confirm option support?
Check the official Puppeteer API documentation for the version installed in your project. The research sources include stable documentation versions 25.11.0 and 25.12.0, while API behavior can vary by version.


