Back to Blog
Guide February 25, 2026 · 5 min read

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 →