The page break you see is a guess: what I learned building an embeddable report designer

Browsers paginate one way, PDF libraries another, and the break a user sees in a preview is a guess about where it will fall in the finished file. This is what that costs, and the five problems underneath it that turned out to be harder than they looked.

Sooner or later, every serious business application needs to produce documents for its customers: statements, client reviews, factsheets, invoices, certificates. Then someone asks whether customers can design their own. That’s usually when a team starts building a report designer, under deadline pressure and without really meaning to.

After twenty years of building business software, I decided to build the one I kept wishing existed, and to do it properly. It’s called BroadPaper. It’s an embeddable report designer for React and Angular apps. You declare the data, your users design the document, and you get a print-ready PDF out the other side.

This article isn’t really a product pitch. It’s about the problems that turned out to be much harder than they looked, because if you’ve ever shipped document generation, you’ve probably met some of them.

What you see is not what prints

Here’s the problem at the centre of all of this. Browsers paginate for print in their own way, largely invisibly. PDF libraries paginate in another way. So the page break your user sees in a designer preview is really a guess about where the break will fall in the final file.

Most of the time the guess is close enough. Then a client’s portfolio has three more holdings than the sample data, a table spills onto a new page, its header doesn’t repeat, and a document goes out looking broken. “It looked fine in the preview” is one of the most expensive sentences in document generation.

So the first decision was the biggest one: neither the browser nor the PDF engine gets to decide where a page breaks. BroadPaper does.

It works like this. Each section of a document is measured once, as one continuous galley: an infinitely tall strip, the way typesetters used to work. A pure function, the paginator, then cuts that galley into clip windows and assigns them to pages. It handles keep-together rules, orphans and widows, repeated table headers, and running headers with page numbers. Both the on-screen canvas and the PDF engine are handed pages that have already been decided. Neither of them does any pagination of its own.

If the preview and the PDF measure with the same engine and the same embedded font files, a break on screen is a break in the file. There’s a test that lays out the same document in real Chromium and in the engine, and fails if any section drifts by more than 12 pixels.

Bugs that don’t crash are the dangerous ones

The engine I build on, Forme, is an open-source (MIT) PDF renderer written in Rust and compiled to WebAssembly. It’s excellent, but like any renderer it has its own ideas. One of them was that anything taller than a page gets clipped, silently.

A clip path was hiding a hundred-row table’s missing rows. Worse, every neighbouring page’s text was still in each page’s content stream, so text extraction (and anything downstream of it, like screen readers or search indexing) read the whole table on every page. The PDF looked fine but wasn’t.

The fix was to crop each section to exactly the window its page shows. A table that has been cut now reports how many rows it dropped above the window, so everything below it can shift up.

The same thing happened at scale. The measuring surface is 200,000 points tall, and a table passes that at around ten thousand rows. Past that, the output was a one-page PDF with no warning at all. Tables over 1,000 rows are now measured in batches and joined back together. That’s exact rather than approximate, because a row’s height never depends on its neighbours.

The lesson I keep relearning: in document generation, a crash is the good outcome. The bad outcome is a plausible-looking document that’s wrong.

Blocks, not a canvas

A lot of report designers give users a free-form canvas: drag anything anywhere, pixel by pixel. It demos beautifully. In production, headings end up three pixels off the grid, tables overlap footers, and every new data shape breaks the layout.

BroadPaper uses a structured document model instead: page, section, row, column, block. Users still drag and drop, bind fields, set conditions and choose a brand. But because the structure is a tree rather than a sheet of coordinates, the output is predictable. A table always knows which column it’s in, and a user can’t break the grid even when they try.

That structure is also what makes custom blocks first-class. You give a block a declarative inspector and a pure render function, and it drags, binds, themes and prints exactly like the built-in ones.

No eval, ever

Documents need calculations, like sum(lines.amount) * (1 + taxRate). The shortcut is to hand that string to JavaScript’s eval and hope. In a product where end users author templates, that’s a security incident waiting to happen.

So expressions go through a hand-written tokeniser, a Pratt parser and a tree-walking interpreter, and they’re type-checked against the host application’s schemas. The designer can autocomplete fields, and it catches a typo before it ever reaches a document. Business users never see the syntax at all: they build calculations by picking steps.

The template is the contract

When a user saves a design, you get back plain JSON that you store wherever you like. Nothing in it depends on a browser. That turned out to be the most useful property in the whole system.

The report a user designs on Monday can run as a nightly batch on Tuesday and sit behind an API endpoint on Wednesday: same template, same paginator, same engine, and the page breaks don’t move. It renders in the browser tab, in Node, or behind a small HTTP render service. There’s also a .NET client for .NET 8 and 10, so a C# back end can produce the same PDFs without a JavaScript runtime or a native PDF library on the box.

There’s no headless Chrome anywhere. The engine is about 7 MB of WebAssembly, loaded only by pages that actually render a PDF, so there’s no browser to install, patch and keep alive on every server.

The SDK also makes no network calls of its own. Your schemas go in, JSON comes out, and your customers’ data never leaves your application.

When you shouldn’t use it

If your developers write every document in code, react-pdf is great and free. If you already have HTML that prints acceptably, Puppeteer may be all you need. BroadPaper is for the situation where your users need to design documents, and those documents have to come out right every time. I’ve written some comparisons, including when BroadPaper is the wrong choice.

Try it

The demo runs entirely in your browser, with no account and nothing uploaded. It includes a finished report, a blank page, three brands, a custom block, and a PDF rendered in your own tab.

There’s also a public sample application on GitHub, with React and Angular front ends and a .NET back end. It installs the published packages the way you would, rather than from inside a monorepo, which is the part that proves the integration really is as small as I claim.

BroadPaper is early, and I’d really value hearing from developers who’ve been through this. Where would the designer’s model break down for the documents your app needs to produce?

Demo and docs: broadpaper.com/developers.

The paginator, in your own browser.

A finished report, a blank page, three brands, a custom block and a real vector PDF made in your tab. No account, nothing uploaded.

Open the demo Read the quick start