Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use a headless browser. In Node.js, Puppeteer can open the URL, wait for it to become ready, and call page.pdf() to create a PDF. The essential sequence is browser launch → page creation → navigation → PDF generation → browser shutdown. The example below writes an A4 file and closes the browser even when navigation or PDF generation fails.

Convert a URL to PDF with Puppeteer

Install Puppeteer in a Node.js project. The package downloads a compatible browser during installation in its normal setup, although deployment environments may require additional browser dependencies or a separately managed executable.

npm install puppeteer

Create url-to-pdf.js:

const puppeteer = require('puppeteer');

async function saveUrlAsPdf(url, outputPath) {
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();
    await page.goto(url, { waitUntil: 'networkidle2' });
    await page.pdf({
      path: outputPath,
      format: 'A4'
    });
  } finally {
    await browser.close();
  }
}

saveUrlAsPdf('https://example.com', './page.pdf')
  .catch((error) => {
    console.error('PDF generation failed:', error);
    process.exitCode = 1;
  });

Run it with:

node url-to-pdf.js

The relative output path is resolved from the process’s current working directory. A successful run creates page.pdf there. The finally block is important for long-running services: it prevents a failed request from leaving a browser process behind.

What each step does

  1. Launch: puppeteer.launch() starts a Chromium-based browser.
  2. Create a page: browser.newPage() gives the operation an isolated tab.
  3. Navigate: page.goto() requests the URL. The example uses the documented networkidle2 condition, which waits until network activity is low.
  4. Render: page.pdf() prints the rendered page and writes the result to the supplied path.
  5. Close: browser.close() releases the browser and its child processes.

Make the PDF look like the page you intend

Print CSS versus screen CSS

page.pdf() uses print media by default. That is usually desirable for a document, because sites often define print-specific margins, navigation removal, and typography. If the PDF should match the on-screen design instead, emulate screen media before calling page.pdf():

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.emulateMediaType('screen');
await page.pdf({ path: './screen-style.pdf', format: 'A4' });

Playwright uses the equivalent page.emulateMedia({ media: 'screen' }) API.

Paper size, dimensions, and orientation

Puppeteer accepts a format such as A4, Letter, or Legal. Its documented default is Letter. When format is present, it takes priority over explicit width and height. Use one approach deliberately:

await page.pdf({
  path: './report.pdf',
  format: 'A4',
  landscape: true,
  printBackground: true,
  margin: {
    top: '12mm',
    right: '12mm',
    bottom: '12mm',
    left: '12mm'
  }
});

For a custom canvas, omit format and provide width and height. Do not expect those values to override a format selected at the same time.

Backgrounds and exact colors

Printing can alter colors and background treatment. Set printBackground: true when colored panels, images, or backgrounds are part of the document. A page can also request more exact color output with this CSS:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
@media print {
  * {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
}

The final result still depends on the page’s CSS, assets, and browser rendering.

Fonts and images

Puppeteer’s documentation states that PDF generation waits for fonts by default. Images and application data have different readiness requirements. A single universal wait condition cannot guarantee that every dynamic site has finished rendering, so add a page-specific readiness check when necessary.

await page.goto(url, { waitUntil: 'networkidle2' });
await page.waitForSelector('#report-ready');
await page.pdf({ path: './report.pdf', format: 'A4' });

If the page has no reliable marker, a short delay can help with late animations, but it is less precise than waiting for a selector or application event:

await new Promise((resolve) => setTimeout(resolve, 1000));

Return PDF bytes instead of writing a file

For an HTTP endpoint, you may want to send the PDF directly. Puppeteer documents a Uint8Array result when no path is required:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const puppeteer = require('puppeteer');

async function pdfBytes(url) {
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();
    await page.goto(url, { waitUntil: 'networkidle2' });
    return await page.pdf({ format: 'A4' });
  } finally {
    await browser.close();
  }
}

(async () => {
  const bytes = await pdfBytes('https://example.com');
  require('fs').writeFileSync('./page.pdf', bytes);
})();

In an Express handler, send the returned bytes with Content-Type: application/pdf and a suitable Content-Disposition header. Keep browser creation outside the request path only when you have a carefully managed browser pool; otherwise, always enforce timeouts and cleanup.

Playwright alternative

Playwright also renders a browser page and exposes page.pdf(). Choose it when your application already uses Playwright or needs its browser and context APIs. The basic flow is the same:

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  try {
    const page = await browser.newPage();
    await page.goto('https://example.com', { waitUntil: 'networkidle' });
    await page.pdf({ path: './page.pdf', format: 'A4' });
  } finally {
    await browser.close();
  }
})();

Playwright documents a returned PDF buffer, and its screen-media call is await page.emulateMedia({ media: 'screen' }). There is no documented general performance winner between Puppeteer and Playwright in the material available here. Base the choice on the library already used by your application, the browser/runtime supported by your deployment, and whether you prefer a file path or returned buffer.

When PDFKit is the better fit

PDFKit takes a different approach: you construct PDF content programmatically and pipe a PDFDocument to a writable stream. It is appropriate for invoices, certificates, or reports whose layout you control completely. It does not print an existing HTML page, so it will not automatically reproduce a site’s CSS, client-side JavaScript, or responsive layout. Use a browser API when the source of truth is an HTML URL.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Or skip the browser setup

ScreenshotNeo provides a website screenshot and PDF API. One GET request can return a PDF for a URL, without installing Chromium or maintaining browser processes. Its capture pipeline accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing result in X-Page-Verdict and X-Billed headers.

For the complete parameter list and PDF options, see the ScreenshotNeo documentation. A cURL request is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -d format=pdf -o page.pdf

The same request from Python:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={
        "access_key": "YOUR_API_KEY",
        "url": "https://example.com",
        "format": "pdf"
    },
    timeout=90
)
r.raise_for_status()
open("page.pdf", "wb").write(r.content)

And Node.js:

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com',
  format: 'pdf'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`ScreenshotNeo returned ${res.status}`);
require('fs').writeFileSync('page.pdf', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Its PDF controls include paper size, margins, landscape mode, and page ranges; the service also supports custom CSS and JavaScript, headers, cookies, user agents, authorization, waiting rules, request blocking, caching, and bulk capture.

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account to try the PDF endpoint.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting Node.js HTML-to-PDF conversion

“Cannot find module ‘puppeteer’”

Install the dependency in the same project and environment that runs the script: npm install puppeteer. Check that you are running the command from the project directory and that production deployment did not omit the dependency.

Browser fails to launch in a container

Minimal Linux images often lack libraries required by Chromium or impose sandbox restrictions. Use a browser-compatible base image, install the required system packages, and follow your deployment platform’s browser guidance. Do not blindly add --no-sandbox; it weakens isolation and should be considered only when you understand the container’s security model.

The PDF is blank or missing late content

Navigation completion is not the same as application readiness. Wait for a meaningful selector, data state, or page-specific event after page.goto(). Check that the URL is reachable from the server, not only from your laptop, and inspect console or network errors.

Styles differ from the browser view

Remember that PDF generation uses print media. Use emulateMediaType('screen') for screen CSS, set printBackground: true for backgrounds, and inspect print rules that hide or rearrange content.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Navigation never finishes

Some pages keep connections open for analytics, streaming, or advertisements. Use an explicit navigation timeout and a readiness selector rather than waiting indefinitely:

page.setDefaultNavigationTimeout(30000);
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('#report-ready', { timeout: 15000 });

Choose the readiness condition according to the application; changing to a weaker event can produce a PDF before data is present.

PDF files are unexpectedly large

Large images, web fonts, and embedded resources contribute to size. Optimize source assets where you control them, avoid waiting for content that should not be printed, and use a deliberate page format and margin configuration. Do not remove fonts or images blindly if visual fidelity is required.

Operational guidance for reliable jobs

  • Validate and allow-list destination URLs when users supply them; unrestricted URL fetching can expose internal services.
  • Set navigation and selector timeouts, and return a clear error when a page does not become ready.
  • Always close pages and browsers in finally blocks.
  • Use unique output names and write to a directory with sufficient space.
  • Log the URL, elapsed time, selected readiness condition, and failure reason without recording secrets embedded in query strings.
  • Test representative pages containing print CSS, client-rendered data, lazy images, web fonts, iframes, and authentication.
  • For repeated jobs, reuse a controlled browser process or pool only after measuring resource usage and enforcing per-job isolation.

FAQ

Does Puppeteer convert only public URLs?

No. It can navigate to any address reachable from the machine, provided authentication, DNS, firewall, and application requirements are satisfied. Protect user-controlled URL input before enabling arbitrary capture.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can I select a range of PDF pages in Puppeteer?

The core example does not set a page range. If your Puppeteer version exposes a page-range option, consult that version’s API documentation and test it against the generated document.

Should I use A4 or Letter?

Use the paper standard required by your readers or downstream workflow. Puppeteer’s documented default is Letter; specifying format: 'A4' makes the choice explicit.

Frequently Asked Questions

Does Puppeteer wait for web fonts before creating the PDF?

Puppeteer documentation says that PDF generation waits for fonts to be loaded by default. Other dynamic content still needs an application-specific readiness check.

What is the simplest managed option if I do not want Chromium in my deployment?

ScreenshotNeo’s PDF endpoint accepts a URL over HTTPS, removes supported consent banners and widgets before capture, and does not bill failed loads, blank pages, bot checks, or timeouts.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.