Tools: How to generate a PDF from HTML in Node.js (without Puppeteer)

Tools: How to generate a PDF from HTML in Node.js (without Puppeteer)

How to Generate a PDF from HTML in Node.js (Without Puppeteer) ## Basic usage ## Capture a live URL instead ## Use in an Express route ## Use in AWS Lambda / Vercel Functions ## CSS in generated PDFs ## Beyond PDFs: add a narrated walkthrough The canonical Node.js answer for HTML-to-PDF is Puppeteer: spin up a headless Chromium, navigate to a page or set content, call page.pdf(). It works, but it pulls Chromium into your dependency tree, adds 200–400MB to your deployment, and breaks in serverless environments unless you configure a Chromium layer. Here's the one-fetch alternative: No browser. No Chromium. One fetch, one file. If the content is already on a URL (a hosted invoice, a report page, a dashboard), pass url instead of html: No extra config needed. The capture is an outbound HTTPS call — it runs in any serverless environment without Chromium layers, memory tuning, or cold-start mitigation. All CSS in your <style> block renders in the PDF. A few practical notes: If you want to show users how a document was generated — useful for invoice portals or report builders — record a narrated video of the flow in the same API call pattern: Same pattern, one API, no extra tools. Try it free — 100 requests/month, no credit card. → pagebolt.dev Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to ? It will become hidden in your post, but will still be visible via the comment's permalink. as well , this person and/or CODE_BLOCK: import fs from 'fs'; const html = `<!DOCTYPE html> <html> <head> <style> body { font-family: system-ui, sans-serif; padding: 40px; } h1 { font-size: 24px; margin-bottom: 8px; } .amount { font-size: 32px; font-weight: bold; color: #111; } </style> </head> <body> <h1>Invoice #1042</h1> <p>Due: March 1, 2026</p> <div class="amount">$429.00</div> </body> </html>`; const res = await fetch('https://pagebolt.dev/api/v1/pdf', { method: 'POST', headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ html }) }); fs.writeFileSync('invoice.pdf', Buffer.from(await res.arrayBuffer())); CODE_BLOCK: import fs from 'fs'; const html = `<!DOCTYPE html> <html> <head> <style> body { font-family: system-ui, sans-serif; padding: 40px; } h1 { font-size: 24px; margin-bottom: 8px; } .amount { font-size: 32px; font-weight: bold; color: #111; } </style> </head> <body> <h1>Invoice #1042</h1> <p>Due: March 1, 2026</p> <div class="amount">$429.00</div> </body> </html>`; const res = await fetch('https://pagebolt.dev/api/v1/pdf', { method: 'POST', headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ html }) }); fs.writeFileSync('invoice.pdf', Buffer.from(await res.arrayBuffer())); CODE_BLOCK: import fs from 'fs'; const html = `<!DOCTYPE html> <html> <head> <style> body { font-family: system-ui, sans-serif; padding: 40px; } h1 { font-size: 24px; margin-bottom: 8px; } .amount { font-size: 32px; font-weight: bold; color: #111; } </style> </head> <body> <h1>Invoice #1042</h1> <p>Due: March 1, 2026</p> <div class="amount">$429.00</div> </body> </html>`; const res = await fetch('https://pagebolt.dev/api/v1/pdf', { method: 'POST', headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ html }) }); fs.writeFileSync('invoice.pdf', Buffer.from(await res.arrayBuffer())); CODE_BLOCK: const res = await fetch('https://pagebolt.dev/api/v1/pdf', { method: 'POST', headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://yourapp.com/invoices/1042', blockBanners: true }) }); CODE_BLOCK: const res = await fetch('https://pagebolt.dev/api/v1/pdf', { method: 'POST', headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://yourapp.com/invoices/1042', blockBanners: true }) }); CODE_BLOCK: const res = await fetch('https://pagebolt.dev/api/v1/pdf', { method: 'POST', headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://yourapp.com/invoices/1042', blockBanners: true }) }); COMMAND_BLOCK: import express from 'express'; const app = express(); app.get('/invoices/:id/pdf', async (req, res) => { const invoice = await getInvoice(req.params.id); // your data fetch const html = renderInvoiceHtml(invoice); // your template function const capture = await fetch('https://pagebolt.dev/api/v1/pdf', { method: 'POST', headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ html }) }); const pdf = Buffer.from(await capture.arrayBuffer()); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `attachment; filename="invoice-${req.params.id}.pdf"`); res.send(pdf); }); COMMAND_BLOCK: import express from 'express'; const app = express(); app.get('/invoices/:id/pdf', async (req, res) => { const invoice = await getInvoice(req.params.id); // your data fetch const html = renderInvoiceHtml(invoice); // your template function const capture = await fetch('https://pagebolt.dev/api/v1/pdf', { method: 'POST', headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ html }) }); const pdf = Buffer.from(await capture.arrayBuffer()); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `attachment; filename="invoice-${req.params.id}.pdf"`); res.send(pdf); }); COMMAND_BLOCK: import express from 'express'; const app = express(); app.get('/invoices/:id/pdf', async (req, res) => { const invoice = await getInvoice(req.params.id); // your data fetch const html = renderInvoiceHtml(invoice); // your template function const capture = await fetch('https://pagebolt.dev/api/v1/pdf', { method: 'POST', headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ html }) }); const pdf = Buffer.from(await capture.arrayBuffer()); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `attachment; filename="invoice-${req.params.id}.pdf"`); res.send(pdf); }); COMMAND_BLOCK: // Lambda handler export const handler = async (event) => { const { html } = JSON.parse(event.body); const res = await fetch('https://pagebolt.dev/api/v1/pdf', { method: 'POST', headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ html }) }); const pdf = Buffer.from(await res.arrayBuffer()); return { statusCode: 200, headers: { 'Content-Type': 'application/pdf' }, body: pdf.toString('base64'), isBase64Encoded: true }; }; COMMAND_BLOCK: // Lambda handler export const handler = async (event) => { const { html } = JSON.parse(event.body); const res = await fetch('https://pagebolt.dev/api/v1/pdf', { method: 'POST', headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ html }) }); const pdf = Buffer.from(await res.arrayBuffer()); return { statusCode: 200, headers: { 'Content-Type': 'application/pdf' }, body: pdf.toString('base64'), isBase64Encoded: true }; }; COMMAND_BLOCK: // Lambda handler export const handler = async (event) => { const { html } = JSON.parse(event.body); const res = await fetch('https://pagebolt.dev/api/v1/pdf', { method: 'POST', headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ html }) }); const pdf = Buffer.from(await res.arrayBuffer()); return { statusCode: 200, headers: { 'Content-Type': 'application/pdf' }, body: pdf.toString('base64'), isBase64Encoded: true }; }; CODE_BLOCK: const res = await fetch('https://pagebolt.dev/api/v1/video', { method: 'POST', headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ steps: [ { action: 'navigate', url: 'https://yourapp.com/invoices/1042', note: 'Open the invoice' }, { action: 'click', selector: '#download-pdf', note: 'Download as PDF' } ], audioGuide: { enabled: true, voice: 'nova', script: "Here's your invoice. {{1}} {{2}} One click to download." }, pace: 'slow' }) }); fs.writeFileSync('invoice-demo.mp4', Buffer.from(await res.arrayBuffer())); CODE_BLOCK: const res = await fetch('https://pagebolt.dev/api/v1/video', { method: 'POST', headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ steps: [ { action: 'navigate', url: 'https://yourapp.com/invoices/1042', note: 'Open the invoice' }, { action: 'click', selector: '#download-pdf', note: 'Download as PDF' } ], audioGuide: { enabled: true, voice: 'nova', script: "Here's your invoice. {{1}} {{2}} One click to download." }, pace: 'slow' }) }); fs.writeFileSync('invoice-demo.mp4', Buffer.from(await res.arrayBuffer())); CODE_BLOCK: const res = await fetch('https://pagebolt.dev/api/v1/video', { method: 'POST', headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ steps: [ { action: 'navigate', url: 'https://yourapp.com/invoices/1042', note: 'Open the invoice' }, { action: 'click', selector: '#download-pdf', note: 'Download as PDF' } ], audioGuide: { enabled: true, voice: 'nova', script: "Here's your invoice. {{1}} {{2}} One click to download." }, pace: 'slow' }) }); fs.writeFileSync('invoice-demo.mp4', Buffer.from(await res.arrayBuffer())); - Use pt or px units — em/rem relative to viewport can behave unexpectedly in print context - For page breaks, use page-break-before: always or break-before: page - Web fonts work if you include a <link> tag pointing to a publicly accessible font URL - Print-specific styles can be added in a @media print {} block