{"post":{"slug":"how-to-generate-a-pdf-2026","title":"How to generate a PDF in 2026","excerpt":"PDF generation covers invoices, reports, and certificates — each with different tooling tradeoffs. This guide picks the right library for your use case and walks through working examples.","category":"code","date":"March 12, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">G</span>enerating PDFs on the server is a common requirement for invoices, receipts, reports, and certificates. The tricky part is picking the right approach — headless browser rendering gives you full CSS control but requires Chrome; JSX-to-PDF libraries are lighter but have layout constraints; low-level libraries are fast but require manually positioning every element. In 2026, the right choice depends on template complexity and deployment environment.\n\n## What changed in 2026\n\n- **Playwright's `page.pdf()`** is now the preferred headless PDF method over Puppeteer — same API, better stability, and it's the same tool you use for e2e tests.\n- **`@react-pdf/renderer` 4.x** added support for `flexbox gap` and `grid` layout, covering the main layout gaps from earlier versions.\n- **Edge/serverless PDF generation** matured: `@vercel/og` handles image generation; for PDFs on serverless, `pdfkit` (no Chrome dependency) is the reliable choice.\n- **`wkhtmltopdf` is deprecated** — do not start new projects with it; use Playwright or Puppeteer instead.\n- **Chromium bundling** in serverless deployments (via `@sparticuz/chromium`) became standard practice for Next.js PDF routes on Vercel/Lambda.\n\n## Approach comparison\n\n| Approach | Full CSS | Server req | Bundle size | Best for |\n| --- | --- | --- | --- | --- |\n| Playwright / Puppeteer | Yes | Chrome | ~150 MB | Rich templates, charts |\n| `@react-pdf/renderer` | Partial | No | ~2 MB | Invoice-style templates |\n| `pdfkit` | No (programmatic) | No | ~1 MB | Simple documents |\n| `reportlab` (Python) | No (programmatic) | No | ~5 MB | Python backends |\n| HTML email → PDF | Partial | Chrome | ~150 MB | Existing email templates |\n\n## HTML-to-PDF with Playwright\n\n```ts\n// lib/pdf.ts\nimport { chromium } from 'playwright'\n\nexport async function htmlToPdf(html: string): Promise<Buffer> {\n  const browser = await chromium.launch()\n  const page = await browser.newPage()\n\n  await page.setContent(html, { waitUntil: 'networkidle' })\n\n  const pdf = await page.pdf({\n    format: 'A4',\n    printBackground: true,\n    margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },\n  })\n\n  await browser.close()\n  return Buffer.from(pdf)\n}\n```\n\n```ts\n// app/api/invoice/[id]/route.ts\nimport { htmlToPdf } from '@/lib/pdf'\nimport { renderInvoiceHtml } from '@/lib/templates'\n\nexport async function GET(req: Request, { params }: { params: { id: string } }) {\n  const invoice = await getInvoice(params.id)\n  const html    = renderInvoiceHtml(invoice)\n  const pdf     = await htmlToPdf(html)\n\n  return new Response(pdf, {\n    headers: {\n      'Content-Type':        'application/pdf',\n      'Content-Disposition': `attachment; filename=\"invoice-${params.id}.pdf\"`,\n    },\n  })\n}\n```\n\n## React PDF (`@react-pdf/renderer`)\n\n```bash\nnpm install @react-pdf/renderer\n```\n\n```tsx\nimport { Document, Page, Text, View, StyleSheet } from '@react-pdf/renderer'\n\nconst styles = StyleSheet.create({\n  page:    { padding: 40, fontFamily: 'Helvetica' },\n  heading: { fontSize: 24, marginBottom: 16 },\n  row:     { flexDirection: 'row', borderBottom: '1px solid #eee', padding: '8px 0' },\n  cell:    { flex: 1, fontSize: 11 },\n})\n\nexport function InvoicePdf({ invoice }: { invoice: Invoice }) {\n  return (\n    <Document>\n      <Page size=\"A4\" style={styles.page}>\n        <Text style={styles.heading}>Invoice #{invoice.number}</Text>\n        {invoice.lineItems.map(item => (\n          <View key={item.id} style={styles.row}>\n            <Text style={styles.cell}>{item.description}</Text>\n            <Text style={styles.cell}>{item.qty}</Text>\n            <Text style={styles.cell}>${item.total.toFixed(2)}</Text>\n          </View>\n        ))}\n      </Page>\n    </Document>\n  )\n}\n\n// Server-side: render to buffer\nimport { renderToBuffer } from '@react-pdf/renderer'\nconst pdfBuffer = await renderToBuffer(<InvoicePdf invoice={invoice} />)\n```\n\n## pdfkit (lightweight, no browser)\n\n```ts\nimport PDFDocument from 'pdfkit'\nimport { Writable } from 'stream'\n\nfunction generateSimplePdf(title: string, lines: string[]): Promise<Buffer> {\n  return new Promise((resolve, reject) => {\n    const doc = new PDFDocument({ margin: 50 })\n    const chunks: Buffer[] = []\n\n    doc.pipe(new Writable({\n      write(chunk, _, cb) { chunks.push(chunk); cb() },\n      final(cb) { resolve(Buffer.concat(chunks)); cb() },\n    }))\n\n    doc.fontSize(24).text(title, { align: 'center' })\n    doc.moveDown()\n    lines.forEach(line => doc.fontSize(12).text(line))\n    doc.end()\n  })\n}\n```\n\n## Serverless with `@sparticuz/chromium`\n\n```ts\n// For Vercel/Lambda where system Chrome is not available\nimport chromium from '@sparticuz/chromium'\nimport puppeteer from 'puppeteer-core'\n\nconst browser = await puppeteer.launch({\n  args: chromium.args,\n  executablePath: await chromium.executablePath(),\n  headless: chromium.headless,\n})\n```\n\n## How to pick\n\n| Use case | Recommended tool |\n| --- | --- |\n| Invoice / receipt template | `@react-pdf/renderer` |\n| Report with charts | Playwright (render charts in browser) |\n| Simple text document | `pdfkit` |\n| Existing HTML page to PDF | Playwright |\n| Serverless / edge | `pdfkit` or `@react-pdf/renderer` |\n| Python backend | `reportlab` or `weasyprint` |\n\n## Common mistakes\n\n**Not waiting for fonts/images to load** — `waitUntil: 'networkidle'` in Playwright ensures external resources finish loading before capture. Without it, images appear blank.\n\n**Hardcoding page breaks** — use CSS `page-break-after: always` or `break-after: page` for logical breaks; don't try to calculate when content overflows manually.\n\n**Opening a new browser per request** — keep the Playwright/Puppeteer browser instance alive across requests or use a pool; cold-starting Chrome on every PDF request adds ~2 seconds of latency.\n\n**Missing `printBackground: true`** — without it, background colours and images are stripped from the PDF output.\n\n## What to skip\n\n- **`wkhtmltopdf`** — abandoned upstream, poor CSS support, fails on modern web features.\n- **`html-pdf` (npm)** — wraps wkhtmltopdf; same problems.\n- **Generating PDFs on the client** — it works in theory but file size, font embedding, and page layout are unreliable in pure browser JS. Generate server-side and serve the result.\n\n## FAQ\n\n**Can I generate PDFs in a Next.js API route?**\nYes. Use `@react-pdf/renderer` (no Chrome required) or `pdfkit` for serverless compatibility. For Playwright, deploy a separate service or use `@sparticuz/chromium` on Lambda/Vercel.\n\n**How do I add custom fonts?**\nPlaywright: embed via CSS `@font-face` in your HTML. `@react-pdf/renderer`: use `Font.register({ family: 'MyFont', src: url })`. `pdfkit`: `doc.registerFont('MyFont', path)`.\n\n**What about password-protecting the generated PDF?**\n`pdfkit` supports `doc = new PDFDocument({ userPassword: 'secret', ownerPassword: 'admin' })`. Playwright-generated PDFs need post-processing with `qpdf` or `gs` (Ghostscript).\n\n**How large do PDF files get?**\nA plain text invoice is typically 20–80 KB. Add images or custom fonts and it grows to 200 KB–2 MB. Compress images before embedding and subset fonts to keep files small.\n\n## Where to go next\n\n- [How to parse a PDF in 2026](/blog/how-to-parse-a-pdf-2026)\n- [How to build a RSS feed in 2026](/blog/how-to-build-a-rss-feed-2026)\n- [How to set up Stripe payments in 2026](/blog/how-to-set-up-stripe-payments-2026)","hue":306,"metaTitle":"How to Generate a PDF in 2026 (Node.js + React)","metaDescription":"Complete 2026 guide to PDF generation. Puppeteer, Playwright, react-pdf, pdfkit, and Python reportlab — with working invoice and report examples.","keywords":["how to generate a pdf 2026","pdf generation node.js 2026","puppeteer pdf generation","playwright pdf 2026","react-pdf tutorial 2026","pdfkit node.js","invoice pdf javascript","html to pdf 2026","pdf generation python 2026","generate pdf next.js"],"tldr":["**HTML-to-PDF via Puppeteer or Playwright** is the most flexible approach — design in CSS, generate with a headless browser.","**`@react-pdf/renderer`** lets you write PDF layouts in JSX — good for templates that need programmatic data, like invoices.","**`pdfkit` (Node)** or **`reportlab` (Python)** are the low-level, no-dependency options for simple document structures.","**Gotchas:** headless browser PDFs require a server with Chrome available; `@react-pdf/renderer` does not support all CSS properties.","**Skip:** using Word-to-PDF conversion pipelines for new projects — HTML/CSS templates are easier to maintain and version."],"readTime":"4 min read","wordCount":989},"related":[{"slug":"how-to-build-a-rss-feed-2026","title":"How to build a RSS feed in 2026","excerpt":"RSS feeds are having a quiet resurgence in 2026 — newsletters, podcasts, and AI aggregators all consume them. This guide covers building a valid RSS 2.0 and Atom feed for any web framework.","category":"code","date":"April 14, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":322},{"slug":"how-to-build-a-website-2026","title":"How to build a website in 2026: from zero to live in a weekend","excerpt":"Building a website in 2026 is faster than ever, but the options are more confusing. This guide picks the right path for your goal and skill level.","category":"code","date":"March 28, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":306},{"slug":"how-to-parse-a-pdf-2026","title":"How to parse a PDF in 2026","excerpt":"PDF parsing remains surprisingly tricky — text extraction, table detection, and scanned-image handling all need different approaches. This guide maps the right tool to your use case.","category":"code","date":"February 25, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":305}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"how-to-free-up-storage-2026","title":"How to free up storage in 2026: the fastest ways on any device","excerpt":"Running out of storage slows every device down. This 2026 guide covers the fastest, safest cleanup methods for Windows, macOS, Android, and iPhone.","category":"tech","date":"March 12, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":240},"older":{"slug":"how-to-get-pre-approved-for-a-mortgage-2026","title":"How to get pre-approved for a mortgage in 2026: what lenders check","excerpt":"Mortgage pre-approval is more than a formality — it determines your buying power, your rate, and whether sellers take your offer seriously in 2026.","category":"finance","date":"March 12, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":292}}