1
0 Comments

I replaced a PDF plugin with a custom Cloud Run + Puppeteer pipeline

Build Report:

I’ve been building Sheeto, a web app for creating printable worksheets through a chat-based editor.

One of the hardest features has been PDF export.

At first, I used an existing PDF plugin. It worked for simple pages, but it became unreliable as the editor grew more complex.

I needed the exported PDF to preserve:

  • Exact worksheet dimensions

  • User-selected paper sizes and margins

  • Images and web fonts

  • Model answers and handwriting-style text

  • Object-level page breaks

  • Multi-page worksheet layouts

  • Separate answer sheets

The plugin could not reliably reproduce everything shown in the editor, so I started building my own PDF system.

Building page breaks first

Before replacing the PDF exporter, I built a custom page-break system inside the editor.

Each worksheet consists of multiple objects, such as headings, questions, images, answer fields and writing areas.

The editor measures these objects and determines which object should begin the next page. This means page breaks happen between meaningful worksheet objects instead of cutting through their contents.

The resulting page-break positions are saved and reused during PDF generation.

This became important later because calculating page breaks independently in two different browser environments produced inconsistent results.

The editor is now the source of truth for worksheet pagination.

My first Puppeteer approach failed

My first custom implementation sent the worksheet’s generated HTML to a Cloud Run service.

The service opened the HTML in Puppeteer and converted it into a PDF.

The basic pipeline worked, but the actual worksheet did not render consistently.

The exported result could differ because the new browser environment did not automatically inherit everything from the original editor:

  • Loaded CSS

  • Web fonts

  • Images

  • Runtime DOM changes

  • Computed layout

  • Dynamically rendered answers

I realized that “sending the HTML” was not the same as “reproducing the editor.”

I temporarily abandoned the Puppeteer approach and considered using the browser’s native print function instead.

The change that made it work

Before moving on, I tried one more approach.

Instead of sending isolated HTML to Puppeteer, the Cloud Run service now opens the actual Sheeto editor in an authorized session.

Inside that real application page, it waits for the worksheet to finish rendering and then runs the same export logic used by the editor.

The new flow is roughly:

User requests a PDF ↓ A backend workflow starts the job ↓ Cloud Run launches Puppeteer ↓ Puppeteer opens the actual editor ↓ The editor restores the requested worksheet view ↓ Dynamic content, fonts and images finish loading ↓ The editor builds a PDF-only DOM clone ↓ Saved page-break information is applied ↓ Puppeteer generates the PDF ↓ The completed file is downloaded automatically

This avoided rebuilding the application’s visual state from raw HTML.

The existing editor became part of the rendering engine.

Waiting for the page to be truly ready

Another issue was that a page could appear loaded while parts of the worksheet were still being generated.

For example, the first PDF attempt sometimes omitted:

  • Correct-answer circles

  • Writing lines

  • Model answers

  • Parsed fill-in-the-blank answers

Running the export a second time often worked, which showed that this was a timing problem.

I added explicit readiness checks for dynamically generated content. PDF generation now waits for the required answer elements, fonts and layout calculations to finish before cloning the page.

One useful lesson was:

“The page has loaded” and “the page is ready to print” are not the same state.

Adding answer-sheet export

The worksheet and answer sheet use different layouts.

The worksheet follows page breaks selected in the editor. The answer sheet arranges answers more compactly and can place multiple answer objects on the same row.

I therefore added a separate answer-sheet pagination process that:

  • Measures the rendered answer objects

  • Groups objects that share the same visual row

  • Keeps section headings with their content

  • Calculates page breaks using the selected paper size

  • Creates a PDF clone specifically for the answer sheet

The Cloud Run service can now generate either view using the same endpoint.

Improving the export experience

PDF generation can take several seconds, especially when Cloud Run starts a new instance.

To make the delay feel intentional, I added a progress UI in Bubble. The progress slows down near completion instead of moving at a perfectly fixed rate, but it never claims the file is complete until the backend actually finishes.

The current experience is:

  1. Click PDF

  2. See which document is being prepared

  3. Watch the progress indicator

  4. Automatically download the finished file

Worksheet PDFs and answer-sheet PDFs are stored separately, and an older generated file is removed only after its replacement has been saved successfully.

Current result

The custom pipeline now supports:

  • Cloud Run + Puppeteer PDF generation

  • Object-aware page breaks

  • A4 and Letter paper sizes

  • Configurable worksheet margins

  • Dedicated answer-sheet margins

  • Images, backgrounds and web fonts

  • Dynamically generated model answers

  • Separate worksheet and answer-sheet files

  • Automatic download

  • Progress feedback

  • Previous-file cleanup

There are still occasional layout inconsistencies in the answer-sheet export. For now, I’ve accepted that as a known limitation.

In the longer term, I may restructure the answer sheet so that it uses exactly the same page-based rendering model as the worksheet. That should make both exports even more consistent.

What I learned

The biggest lesson was that the first failed approach was not proof that Puppeteer could not work.

The real problem was the boundary I had chosen.

Trying to reconstruct the editor from exported HTML was fragile. Opening the real application and reusing its existing rendering logic was much more reliable.

I also learned that duplicating layout calculations across environments creates unnecessary instability. Having one source of truth for pagination made the system easier to reason about.

This feature took several iterations and, at one point, I had almost given up on fully automated PDF generation.

It is now working well enough to become a real part of Sheeto.

The next step is to separate the staging and production PDF services so I can continue improving the renderer without affecting live users.

posted toAvatar for product Sheeto
Sheeto