How to Generate a PDF from a React Component (Without a Headless Browser)
The standard approach for PDF generation from React is painful: Puppeteer in your server, Chromium in Docker, rendering differences between environments. Here's a cleaner path: render to HTML, send to a capture API, get a PDF back.
The standard approach for PDF generation from React is painful: run Puppeteer or Playwright in your server, keep a headless browser warm, deal with Chromium installation in Docker, and debug rendering differences between your dev machine and production.
There's a cleaner path: render your component to HTML, POST it to a capture API, get a PDF back. No browser process, no Chromium layer, no per-environment debugging.
Render to HTML, capture to PDF
The pattern works in two steps: use renderToStaticMarkup (or a full SSR render) to get HTML from your React component, then send it to PageBolt's /pdf endpoint.
import { renderToStaticMarkup } from 'react-dom/server';
async function generatePDF(component) {
// Step 1: render component to HTML string
const html = renderToStaticMarkup(component);
// Wrap with base styles so the PDF renders cleanly
const fullHtml = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: system-ui, sans-serif; margin: 0; padding: 32px; }
</style>
</head>
<body>${html}</body>
</html>`;
// Step 2: send to capture API, get PDF back
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: fullHtml })
});
return Buffer.from(await res.arrayBuffer());
}
Use it in a Next.js API route:
// pages/api/invoice/[id].js
import { renderToStaticMarkup } from 'react-dom/server';
import { InvoiceTemplate } from '@/components/InvoiceTemplate';
export default async function handler(req, res) {
const invoice = await getInvoice(req.query.id);
const pdf = await generatePDF(<InvoiceTemplate invoice={invoice} />);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="invoice-${invoice.id}.pdf"`);
res.send(pdf);
}
What about CSS?
renderToStaticMarkup outputs unstyled HTML — your Tailwind classes, CSS modules, and styled-components won't be inlined automatically. Three options:
Option 1: Inline styles. Write the PDF template with inline styles from the start. Verbose, but portable.
Option 2: Inject a stylesheet. Include a <link rel="stylesheet"> pointing to a publicly accessible CSS file in the <head>. Works well for design-system components.
Option 3: Inject critical CSS inline. Extract critical CSS with a tool like critical or manually copy the relevant rules into a <style> block in your HTML string. Reliable for isolated templates.
For invoice and report templates, inline styles tend to be the most predictable — you're not fighting cascade specificity in a rendering context you don't fully control.
App Router (Next.js 13+)
In the App Router, use a Route Handler:
// app/api/invoice/[id]/route.js
import { renderToStaticMarkup } from 'react-dom/server';
import { InvoiceTemplate } from '@/components/InvoiceTemplate';
export async function GET(request, { params }) {
const invoice = await getInvoice(params.id);
const html = renderToStaticMarkup(<InvoiceTemplate invoice={invoice} />);
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: `<!DOCTYPE html><html><body>${html}</body></html>` })
});
const pdf = await res.arrayBuffer();
return new Response(pdf, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="invoice-${params.id}.pdf"`
}
});
}
No Puppeteer in your bundle, no browser process to manage, no Chromium layer in your Docker image.
Add a narrated walkthrough
If you want to show users how a PDF was generated — useful for invoice portals, report builders, or documentation tools — record a narrated video of the generation flow:
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/123', note: 'Open invoice' },
{ action: 'click', selector: '#download-pdf', note: 'Click to download PDF' },
{ action: 'wait_for', selector: '.download-complete' }
],
audioGuide: {
enabled: true,
voice: 'nova',
script: "Here's how invoice download works. {{1}} {{2}} One click. {{3}} Your PDF is ready."
},
pace: 'slow'
})
});
Same API, one call, narrated MP4 output.
Get Started Free
100 requests/month, no credit card
Generate PDFs from any HTML or React component — no headless browser required.
Get Your Free API Key →