PDFPipe

Migrating from Puppeteer / Already HTML and CSS

Migrating off self-hosted Puppeteer, an operational migration

The same browser engine on the other side, so nothing about the document changes and everything about running it does.

What ports and what does not

All of it. This is the one migration in the cluster with no CSS delta at all, because the engine rendering your document afterwards is the same kind of engine that was rendering it before. Templates, stylesheets, fonts, JavaScript and page setup all behave identically. What you are migrating is the operational surface: a browser process you were launching, keeping alive, running out of memory, and patching.

What the call becomes

page.setContent followed by page.pdf becomes a POST to /v1/pdf with html. page.goto followed by page.pdf becomes the same call with url instead. The pdf options object maps almost name for name, in snake case: format stays format, landscape stays landscape, printBackground becomes print_background, pageRanges becomes page_ranges, preferCSSPageSize becomes prefer_css_page_size, margin stays margin, scale stays scale, and displayHeaderFooter with headerTemplate and footerTemplate becomes header_html and footer_html, which are supplied together rather than gated by a separate boolean. The class names the templates substitute are the same ones.

Side by side

The old shape and the new one, with the parts that have no counterpart called out in comments rather than quietly omitted.

js
// Before
const browser = await puppeteer.launch({ args: ["--no-sandbox"] });
const page = await browser.newPage();
await page.setContent(html, { waitUntil: "networkidle0" });
const pdf = await page.pdf({
  format: "A4",
  printBackground: true,
  margin: { top: "20mm", bottom: "25mm", left: "15mm", right: "15mm" },
  displayHeaderFooter: true,
  headerTemplate: '<div style="font-size:8pt">Invoice</div>',
  footerTemplate: '<div style="font-size:8pt"><span class="pageNumber"></span></div>',
});
await browser.close();

// After
const res = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PDFPIPE_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    html,
    filename: "invoice.pdf",
    options: {
      format: "A4",
      print_background: true,      // already the default here
      margin: { top: "20mm", bottom: "25mm", left: "15mm", right: "15mm" },
      header_html: '<div style="font-size:8pt">Invoice</div>',
      footer_html: '<div style="font-size:8pt"><span class="pageNumber"></span></div>',
    },
  }),
});
const pdf = Buffer.from(await res.arrayBuffer());

// Note: printBackground defaulted to false in Puppeteer and
// print_background defaults to true here, so a template that never set
// it was rendering without backgrounds and will now render with them.

The CSS delta, in both directions

What the old engine accepted that a current one does not, and what it refused that a current one wants. The second direction is the one people forget, and it is where the value of the migration is.

  • There is none. The same CSS produces the same layout, which is what makes this migration safe enough to do in an afternoon.
  • One default differs and it is visible: printBackground defaulted to false and print_background defaults to true here. A template that relied on the old default to suppress a background will now show it.
  • The waitUntil condition you were choosing has no direct counterpart on the call. If a document depended on networkidle0 specifically, that is the assumption to re-test.

What changes without anything erroring

The dangerous list. Each of these produces a different document and no diagnostic, so none of them are caught by a test that only checks the render succeeded.

  • Backgrounds appearing where they did not before, from the default change above. This is the only routine visual difference in the whole migration.
  • Nothing else about the page. Page counts should match exactly, and if they do not, something other than the engine changed.
  • Latency characteristics, which move from a warm local browser to a network call. That is an operations question rather than a document one.

What to diff before cutting over

Diff page counts and expect them to be identical. This is the one migration where a difference is a defect rather than expected drift, so treat any mismatch as something to explain before cutting over. Then look at any document with a background, since that default change is the one thing that will differ.

The one thing that always breaks

Nothing about the document, which is why the thing that actually goes wrong is the code around it. The synchronous local call becomes a network call that can fail, time out and be retried, and codebases that had never needed error handling around page.pdf frequently ship the migration without adding any.

Frequently asked

Do I have to rewrite my templates to leave Puppeteer?

No. The templates are HTML and CSS and they carry across. What needs attention is the delta above: the places the two engines disagree, and the configuration that used to live outside the document and now lives inside it, or the other way round.

Can I run both for a while?

Yes, and it is the safest way to do it. Put both behind one internal function that takes your data and returns bytes, switch on an environment variable, and run the new path on real traffic while the old one still serves. You get a diff on real documents rather than on fixtures, and you keep a way back that does not involve a deploy.

What about the documents already generated?

Nothing here changes them. They are files that already exist. What is worth deciding before the cutover is whether a regenerated document has to match the original byte for byte or merely say the same thing, because for anything with a legal or audit character the answer is usually to keep the original file rather than to be able to reproduce it.

Other migrations

The nearest neighbours first, then others that started from the same kind of tool, because the model matters more than the language.

Render one of your existing documents through the playground before changing any code. That comparison is the whole of the risk assessment for this migration.