Node.js Tutorial

Puppeteer PDF: Generate PDFs from URLs in Node.js (2026)

April 202611 min readPuppeteer · Playwright · SnapAPI

Generate pixel-perfect PDFs from URLs and raw HTML using Puppeteer. Covers page format, margins, headers/footers, multi-page CSS, authenticated pages, Express endpoints, and when a PDF API is the better choice.

Basic Puppeteer PDF

npm install puppeteer
const puppeteer = require('puppeteer');

async function urlToPdf(url, outputPath) {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });
  const page = await browser.newPage();

  await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });

  await page.pdf({
    path: outputPath,
    format: 'A4',
    printBackground: true,    // Include CSS backgrounds
    margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' }
  });

  await browser.close();
  console.log(`PDF saved: ${outputPath}`);
}

urlToPdf('https://example.com', 'output.pdf');

PDF Options Reference

await page.pdf({
  path: 'output.pdf',         // Omit to get Buffer instead of file
  format: 'A4',               // 'Letter', 'Legal', 'A3', 'A4', 'A5', etc.
  landscape: false,           // true = horizontal
  printBackground: true,      // Include CSS backgrounds and colors
  margin: {
    top: '2cm',
    right: '1.5cm',
    bottom: '2cm',
    left: '1.5cm'
  },
  scale: 1.0,                 // 0.1–2.0, affects zoom level
  pageRanges: '',             // '' = all, '1-3' = first 3 pages only
  preferCSSPageSize: false    // Use @page CSS size instead of format param
});

Puppeteer supports HTML headers and footers with special classes for page numbers and dates.

await page.pdf({
  format: 'A4',
  printBackground: true,
  displayHeaderFooter: true,
  headerTemplate: `
    
Company Confidential
`, footerTemplate: `
Generated by SnapAPI Page of
`, margin: { top: '2cm', bottom: '2cm', left: '1cm', right: '1cm' } });

Special CSS classes for dynamic content in templates:

Generate PDF from Raw HTML

const puppeteer = require('puppeteer');

async function htmlToPdf(html, outputPath) {
  const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox'] });
  const page = await browser.newPage();

  // Set HTML content directly — no URL needed
  await page.setContent(html, { waitUntil: 'networkidle2' });

  // Inject print-specific CSS
  await page.addStyleTag({
    content: `
      @page { size: A4; margin: 2cm; }
      body { font-family: Arial, sans-serif; font-size: 12pt; }
      .no-print { display: none !important; }
      h1 { page-break-before: avoid; }
      table { page-break-inside: avoid; }
    `
  });

  const pdf = await page.pdf({ format: 'A4', printBackground: true });
  require('fs').writeFileSync(outputPath, pdf);

  await browser.close();
  return pdf;
}

const html = `
  
  
  
  
    

Invoice #1042

Amount due: $299.00

ItemQtyPrice
SnapAPI Pro1$79
`; htmlToPdf(html, 'invoice.pdf');

Multi-Page CSS for Clean Breaks

// Inject CSS to control page breaks before generating PDF
await page.addStyleTag({
  content: `
    /* Avoid breaking inside these elements */
    tr, img, pre, blockquote, figure { page-break-inside: avoid; }

    /* Force a new page before these */
    h1, .page-break-before { page-break-before: always; }

    /* Keep heading with the paragraph that follows */
    h2, h3 { page-break-after: avoid; }

    /* Remove shadows/borders that look bad in print */
    * { box-shadow: none !important; }

    /* Ensure links are visible in print */
    a { text-decoration: underline; color: #0066cc; }
  `
});

const pdf = await page.pdf({ format: 'A4', printBackground: true });

PDF from Authenticated Pages

const puppeteer = require('puppeteer');

async function authenticatedPdf(loginUrl, credentials, targetUrl, outputPath) {
  const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox'] });
  const page = await browser.newPage();

  // Step 1: Log in
  await page.goto(loginUrl, { waitUntil: 'networkidle2' });
  await page.type('#email', credentials.email);
  await page.type('#password', credentials.password);
  await Promise.all([
    page.waitForNavigation({ waitUntil: 'networkidle2' }),
    page.click('[type="submit"]')
  ]);

  // Step 2: Generate PDF from authenticated page
  await page.goto(targetUrl, { waitUntil: 'networkidle2' });
  await page.pdf({ path: outputPath, format: 'A4', printBackground: true });

  await browser.close();
}

authenticatedPdf(
  'https://app.example.com/login',
  { email: 'user@co.com', password: process.env.APP_PASS },
  'https://app.example.com/report/2026-Q1',
  'q1-report.pdf'
);

Express PDF Download Endpoint

const express = require('express');
const puppeteer = require('puppeteer');
const app = express();
app.use(express.json());

let browser;
(async () => { browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox'] }); })();

app.post('/pdf', async (req, res) => {
  const { url, html: rawHtml, format = 'A4' } = req.body;
  if (!url && !rawHtml) return res.status(400).json({ error: 'url or html required' });

  try {
    const page = await browser.newPage();

    if (rawHtml) {
      await page.setContent(rawHtml, { waitUntil: 'networkidle2' });
    } else {
      await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
    }

    const pdf = await page.pdf({ format, printBackground: true });
    await page.close();

    res.set({
      'Content-Type': 'application/pdf',
      'Content-Disposition': 'attachment; filename="document.pdf"',
      'Content-Length': pdf.length
    });
    res.send(pdf);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.listen(3000);
// POST /pdf with { "url": "https://example.com" } → downloads PDF

REST API Alternative (No Browser)

On Lambda, Vercel, or Railway, Puppeteer's Chromium binary causes size and cold-start problems. A PDF API eliminates the browser dependency:

// SnapAPI PDF — works on any hosting, no binary needed
const response = await fetch('https://api.snapapi.pics/v1/screenshot', {
  method: 'POST',
  headers: { 'X-Api-Key': process.env.SNAPAPI_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    url: 'https://example.com',
    format: 'pdf',
    full_page: true,
    wait_for: 'networkidle'
  })
});

const { pdf } = await response.json();
// pdf = base64-encoded PDF
const pdfBuffer = Buffer.from(pdf, 'base64');
require('fs').writeFileSync('output.pdf', pdfBuffer);
// HTML → PDF via API (no Chromium needed)
const response = await fetch('https://api.snapapi.pics/v1/screenshot', {
  method: 'POST',
  headers: { 'X-Api-Key': process.env.SNAPAPI_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    html: '<html><body><h1>Invoice</h1></body></html>',
    format: 'pdf',
    width: 794,   // A4 at 96dpi
    height: 1123
  })
});
const { pdf } = await response.json();
Serverless PDF: SnapAPI works on Lambda, Vercel, Cloudflare Workers — just a fetch() call. Free tier: 200 calls/month. Get key →

Puppeteer vs API: When to Use Which

ScenarioPuppeteerSnapAPI
Self-hosted VPS✅ free
AWS Lambda / Vercel⚠️ layer workarounds✅ native
Docker container⚠️ +500MB image✅ no binary
Custom headers/footers✅ full control
Auth pages✅ full control✅ via headers
High concurrency (>10 concurrent)⚠️ memory pressure✅ managed