How to Save a Webpage as PDF with Node.js
Save a webpage as a PDF with Node.js using Puppeteer or Playwright. Control print styling, page layout, readiness, and file or buffer output.
Use a headless Chromium browser from Node.js. With Puppeteer, navigate to the page and call page.pdf({ path: 'page.pdf' }); with Playwright, use Chromium and the same page.pdf() pattern. Both generate PDFs with print CSS media by default. If you want the page’s screen styling, switch to screen media before generating the PDF.
1. Save a page to PDF with Puppeteer
Install Puppeteer, save this as save-pdf.mjs, and run it with Node.js. The PDF is written to page.pdf in the current working directory. This example uses the default print media and waits for navigation to reach networkidle2; readiness needs can vary by site.
npm install puppeteer
// save-pdf.mjs
import puppeteer from 'puppeteer';
const url = process.argv[2] ?? 'https://example.com';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2' });
await page.pdf({ path: 'page.pdf', format: 'A4', printBackground: true });
console.log('Saved page.pdf');
} finally {
await browser.close();
}
// Run: node save-pdf.mjs https://example.com
Puppeteer’s PDF guide documents this launch, navigate, and save flow. Its PDF method waits for fonts by default. See the Puppeteer PDF generation guide and Page.pdf API.
2. Save a page to PDF with Playwright
Install Playwright and its Chromium browser. Save the following as save-pdf.mjs. Playwright returns a PDF buffer from page.pdf(); passing path also writes the file. PDF generation is Chromium-only.
npm install playwright
npx playwright install chromium
// save-pdf.mjs
import { chromium } from 'playwright';
const url = process.argv[2] ?? 'https://example.com';
const browser = await chromium.launch();
try {
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle' });
const pdf = await page.pdf({ path: 'page.pdf', format: 'A4', printBackground: true });
console.log(`Saved page.pdf (${pdf.length} bytes)`);
} finally {
await browser.close();
}
// Run: node save-pdf.mjs https://example.com
Consult the Playwright Page API and PDF export documentation for the documented options and Chromium limitation.
3. Choose when the page is ready
Navigation reaching a load state does not guarantee that every site-specific widget or asynchronous section has finished rendering. If the content you need appears after navigation, wait for a selector that represents that content before generating the PDF. For a page with a known delayed update, a fixed delay is possible, though a selector is usually a more direct signal.
// Puppeteer: wait for a page-specific element
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-report-ready]');
await page.pdf({ path: 'report.pdf', format: 'A4', printBackground: true });
// Playwright equivalent
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.locator('[data-report-ready]').waitFor();
await page.pdf({ path: 'report.pdf', format: 'A4', printBackground: true });
Replace the selector with one that exists on the target page. Use a navigation state appropriate to that site: domcontentloaded can be sufficient for static content, while network-idle states may help when requests settle. Sites with long polling or persistent connections may never become network-idle, so use a meaningful selector or another page-specific condition. Avoid treating any one wait strategy as universal.
4. Keep screen styling or use print styling
PDF output uses print CSS media by default. Print styles can hide navigation, change colors, or rearrange sections. To render screen media instead, set it explicitly before calling pdf().
// Puppeteer
await page.emulateMediaType('screen');
await page.pdf({ path: 'screen-style.pdf', format: 'A4', printBackground: true });
// Playwright
await page.emulateMedia({ media: 'screen' });
await page.pdf({ path: 'screen-style.pdf', format: 'A4', printBackground: true });
Use print media when the site provides print-specific layout. Use screen media when you want its screen rules to apply, while remembering that PDF pagination still affects the result. For exact colors in Puppeteer, its documentation recommends the CSS property -webkit-print-color-adjust. Page styles and browser PDF options both influence the final appearance.
5. Set paper, margins, backgrounds, and page ranges
Choose a paper size such as A4 or Letter, set margins when content is too close to the edge, and enable background printing when colored backgrounds or graphics matter. The available PDF controls differ slightly between libraries; see their API references for the full current option set.
// Puppeteer
await page.pdf({
path: 'report.pdf',
format: 'A4',
landscape: false,
printBackground: true,
margin: { top: '12mm', right: '12mm', bottom: '12mm', left: '12mm' },
});
// Playwright
await page.pdf({
path: 'report.pdf',
format: 'A4',
landscape: false,
printBackground: true,
margin: { top: '12mm', right: '12mm', bottom: '12mm', left: '12mm' },
pageRanges: '1-3',
preferCSSPageSize: true,
});
Playwright documents paper format, dimensions, margins, scale, page ranges, background printing, and preferCSSPageSize. By default, backgrounds are not printed, and CSS @page sizing does not take priority unless preferCSSPageSize is enabled. When CSS controls the paper dimensions, enable that option; otherwise, set the size in the PDF call. Page range syntax and support should be checked in the library API you use.
6. Return PDF bytes instead of writing a file
When serving a PDF from an HTTP endpoint or passing it to another function, omit path and use the returned bytes or buffer. The example below uses Playwright and Node’s built-in HTTP server. It sends the PDF with a download filename.
import { createServer } from 'node:http';
import { chromium } from 'playwright';
const browser = await chromium.launch();
createServer(async (req, res) => {
if (req.url !== '/report.pdf') {
res.writeHead(404).end('Not found');
return;
}
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
const pdf = await page.pdf({ format: 'A4', printBackground: true });
await page.close();
res.writeHead(200, {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="report.pdf"',
'Content-Length': pdf.length,
});
res.end(pdf);
} catch (error) {
res.writeHead(500, { 'Content-Type': 'text/plain' }).end('PDF generation failed');
console.error(error);
}
}).listen(3000, () => console.log('Open http://localhost:3000/report.pdf'));
// Close the browser when shutting down the server in a long-running app.
In production, validate or allowlist destination URLs if callers can supply them, set request and navigation timeouts appropriate to your application, and ensure browser processes are closed during shutdown. A PDF buffer is convenient for streaming or uploading, but it occupies memory until consumed.
7. Troubleshooting
| Symptom | Likely cause | What to change |
|---|---|---|
| PDF looks different from the browser | PDF rendering uses print media by default, and the site may have print CSS. | Use screen media with emulateMediaType('screen') in Puppeteer or emulateMedia({ media: 'screen' }) in Playwright. Check the page’s print rules and PDF layout options. |
| Background colors or images are missing | Background graphics are not printed by default. | Set printBackground: true. For Puppeteer color fidelity, check -webkit-print-color-adjust in the page’s CSS. |
| Text or sections are missing | Content may load asynchronously after the chosen navigation state. | Wait for a selector tied to the required content before calling pdf(); inspect the page state if the selector never appears. |
| Navigation or PDF generation hangs | The page may keep network connections open, or the selected wait state may not occur. | Try a less restrictive navigation condition and wait for a site-specific selector. Set explicit timeouts and handle failures so the browser is closed. |
| Fonts look wrong | Web fonts may not be available or the page may not have reached the desired state. | Confirm the page can load its fonts and content before printing. Puppeteer’s PDF method waits for fonts by default; allow additional site-specific loading if needed. |
| Playwright reports PDF export is unsupported | Playwright PDF generation is Chromium-only. | Launch Chromium for PDF output, as in the example above. |
| Content is clipped or split awkwardly | Paper size, margins, scaling, or page CSS do not suit the content. | Adjust format, margins, landscape, and scale; inspect @page rules and consider Playwright’s preferCSSPageSize. |
| Browser executable is missing | The automation package is installed but its browser is not available in the environment. | Install the browser required by the selected package; for Playwright, the example uses npx playwright install chromium. |
8. Performance, reliability, and cost
PDF creation requires launching or reusing a browser process, loading the page, waiting for the necessary content, and rendering each page. There is no universal speed figure: page weight, scripts, fonts, network conditions, and PDF length all affect completion time. For a single conversion, launch a browser and close it in a finally block. In a service handling repeated requests, reusing a browser can avoid repeated startup work, but create and close pages per job, cap concurrency, and restart unhealthy browser processes.
Set navigation and operation timeouts, record failures, and close pages and browser processes after errors. If a page is untrusted or user-selected, constrain which URLs the service can access and isolate browser execution according to your deployment’s security model. The browser binaries and their resource use are part of the operational cost of this DIY approach; the cited documentation does not establish a standard runtime cost or performance benchmark.
9. Or skip the browser setup
If your job is a PDF rather than a screenshot, use the browser automation steps above. For a screenshot or PDF capture through an API, ScreenshotNeo accepts one GET request and can return a PDF. Its options include PDF paper size, margins, landscape, and page ranges. The API also supports JavaScript, cookies, custom headers, waiting for a selector, and other capture controls. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.pdf
Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. An MCP server lets AI agents use screenshot and PDF capture tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up free for 1,000 screenshots a month, with no card.
10. FAQ
Can Node.js create a PDF without browser automation?
For a webpage rendered with its HTML and CSS, browser automation is the direct approach covered here because it loads the page in Chromium and exposes PDF rendering. A non-browser PDF library does not automatically reproduce a live website’s browser rendering.
Does page.pdf() save a file by itself?
Pass a path to write the PDF to disk. Without a path, Playwright returns a buffer; Puppeteer’s PDF API also returns PDF bytes that can be handled in memory.
Why does the same page produce a different PDF later?
The page’s content, fonts, print styles, network responses, and rendering options can change. Use a stable readiness condition and keep the media type and PDF settings explicit when repeatability matters.
Can I use Playwright Firefox or WebKit for PDF output?
Playwright’s documented PDF generation is Chromium-only, so use Chromium for this workflow.


