DEV Community

Cover image for We Needed to Send Invoices as PDFs. Here's How We Solved It.
Dharan Ganesan
Dharan Ganesan

Posted on

We Needed to Send Invoices as PDFs. Here's How We Solved It.

A few months ago, our finance team came to us with a very reasonable request.

They were manually creating invoices in Google Docs, exporting them as PDFs, and emailing them to clients every week. It took about three hours every Monday, and they wanted it automated.

I remember thinking:

“This is just PDF generation. I’ve built far more complex things than this.”

That confidence lasted about five minutes. If you’ve never built PDFs before, this article is for you. If you have built PDFs before, you already know where this is going — brace yourself, it’s going to hurt a little. 😅

At the time, the request sounded harmless. Automate invoices. Generate PDFs. Ship it.

What we didn’t realize was that this small task would turn into a few full weeks of fighting browsers, fonts, tables, pagination, and our own overconfidence.


Attempt #1: “Let’s Just Use a PDF Library” 🤡

Like most developers, I started with a PDF library that lets you define documents using JSON.

const docDefinition = {
  content: [
    { text: "INVOICE", style: "header" },
    {
      table: {
        body: [
          ["Pro Plan", "$99"],
          ["Extra Users", "$75"],
        ]
      }
    }
  ]
};

pdfMake.createPdf(docDefinition).download("invoice.pdf");
Enter fullscreen mode Exit fullscreen mode

On paper, it sounded reasonable. In practice, it felt like writing CSS inside a spreadsheet. Everything was deeply nested, styling was awkward, and dynamic tables — the most important part of invoices — were incredibly painful to manage.

After a few days, it was obvious this wasn’t going to scale.

Confused Developer

Problems showed up immediately:

  • Tailwind design system was useless — every style had to be rewritten
  • Simple layout changes turned into deeply nested config objects
  • Dynamic content meant unreadable JSON logic
  • Debugging was painful: generate → download → open → squint → repeat

It also looked like it was designed in 2009.


Attempt #2: “What If We Just Print HTML?” 🖨️

Next came the obvious idea: we already had HTML, so why not just use window.print()?

<div id="invoice">
  <h1>Invoice</h1>
  <table>
    <tr><td>Pro Plan</td><td>$99</td></tr>
  </table>
</div>

<button onclick="window.print()">Generate PDF</button>
Enter fullscreen mode Exit fullscreen mode

At first, it felt like magic ✨

We could reuse our existing HTML. Our CSS worked. Things looked mostly correct.

Then we tested across browsers:

  • Chrome: looks fine
  • Safari: weird margins
  • Firefox: table headers disappear
  • Windows Chrome: footer gone

Pagination was complete chaos. Product names on one page, prices on the next. Half a logo appearing at the bottom of a page for no reason.

For internal docs, this might be acceptable. For client-facing invoices, it wasn’t even close.


Attempt #3: “Okay Fine, Headless Chrome” 😐

To fix browser inconsistencies, we moved to Puppeteer — headless Chrome running on the server.

Same browser. Same environment. Same output every time.

import puppeteer from "puppeteer";

async function generateInvoice(html) {
  const browser = await puppeteer.launch({
    args: ["--no-sandbox", "--disable-setuid-sandbox"],
  });

  const page = await browser.newPage();
  await page.setContent(html, { waitUntil: "networkidle0" });

  await page.pdf({
    format: "A4",
    printBackground: true,
    margin: { top: "20mm", bottom: "20mm" },
  });

  await browser.close();
}
Enter fullscreen mode Exit fullscreen mode

Consistency improved, but new problems appeared:

  • Custom fonts were missing on Linux servers
  • Images sometimes loaded, sometimes didn’t
  • Minor CSS changes broke pagination in surprising ways
  • No real control over page breaks

It felt like building IKEA furniture without instructions. Everything technically fit together, but nothing felt solid.

Everything is Fine


The Real Problem (Nobody Explains This)

Eventually, the pattern became clear.

HTML is designed for scrolling. PDFs are designed for pages.

Web Pages PDFs
Infinite scroll Fixed page sizes
Flexible layout Exact layout
No page rules Strict pagination

Browsers try to bridge this gap.

They mostly fail.

Every team ends up rediscovering this the hard way.


The Thing That Finally Worked 🎉

After weeks of frustration, we discovered pdfn, and it completely changed how we generate PDFs. It lets you build invoices, receipts, or contracts as React components with loops, props, and conditionals, while automatically handling pagination, headers, footers, and smart page breaks.

React Components
  (Invoice, Receipt)
         │
         ▼
 @pdfn/react
  - Pagination-aware rendering
  - Smart page breaks
  - Headers & Footers
         │
         ▼
 @pdfn/serve
  - Headless Chromium / Puppeteer
         │
         ▼
 Perfect PDF Output
Enter fullscreen mode Exit fullscreen mode
import { Document, Page } from "@pdfn/react";

export default function Invoice() {
  return (
    <Document
      title="Invoice #001"
      fonts={[
        // Google Fonts (loaded from CDN)
        "Inter",
        { family: "Roboto Mono", weights: [400, 700] },
        // Local fonts (embedded as base64)
        { family: "CustomFont", src: "./fonts/custom.woff2", weight: 400 },
      ]}
    >
      <Page size="A4" margin="1in">
         <AvoidBreak>
            <Table />
         </AvoidBreak>
      </Page>
    </Document>
  );
}
Enter fullscreen mode Exit fullscreen mode

This architecture makes it easy to maintain and debug your PDF templates.

Why pdfn Felt Different

  • You write React, not JSON
  • You use Tailwind, not a custom styling system
  • Pagination is built-in, not hacked with CSS
  • Headers and footers work by default
  • Smart page breaks (no mid-row or mid-paragraph splits)
  • Dynamic page numbers (Page 1 of 5 works automatically)
  • Live preview and hot reload
  • Debug overlays to visualize margins, grid, and page breaks

The code was readable. The output was predictable.

pdfn is open source (MIT licensed), so there’s no vendor lock-in or surprise pricing later. Inspect the code, self-host it, and adapt it to your needs.


Where We Are Now

Today, our entire PDF workflow is fully automated. Invoices, receipts, agreements, and onboarding forms generate automatically. Finance no longer opens Google Docs, fixes spacing, downloads, or emails PDFs by hand.


TL;DR (For Tired Developers)

  • PDFs are deceptively hard
  • HTML-to-PDF sounds easy (it isn’t)
  • Browser printing will betray you
  • Page-aware tools matter

If you’re about to build PDF generation, please learn from my mistakes.

PDFs are hard. You’re not bad at your job.

Why We're Sharing This

pdfn is relatively new with a small but responsive team. The maintainers answered our issues. The library is actively maintained. It's MIT licensed, so we're not locked in.

More developers need to know it exists.

If this helped:

  • ⭐ Star github.com/pdfnjs/pdfn
  • 📝 Share your use case if you try it
  • 🐛 Report issues to help improve it
  • 💬 Share this article with teams stuck in PDF hell

Open source only works when we share what works.

Community

Top comments (0)