report-pdf — paged HTML → consulting-grade PDF
Turns a Soapbox report/deck template (client-JS-populated HTML) into a paginated
US-Letter PDF with proper print vocabulary: a full-bleed cover header on page
1, a discreet running header on every subsequent page, a footer with
Page N of M, and keep-together rules so tables/charts/cards never split
across a page break or overflow the sheet.
When to use
- "Export / download / save this report as a PDF", "PDF version", "print-ready",
"leave-behind", "send me the deck as a file".
- After any report render (
fill_report) when the user wants the PDF, not the
interactive artifact.
Do not use it to author a new template — that's soapbox-report. This skill
only paginates an existing template's rendered output.
How to run
python3 scripts/export_pdf.py \
--template <name> \
--data <path/to/report-data.json> \
--out <output.pdf> \
[--title "Client — Report Title"] \
[--mode auto|report|deck] \
[--templates-dir <dir>] [--assets-dir <dir>] [--timeout 45000]
--mode (default auto): report = continuous doc via Paged.js (hero page 1,
running header 2+, footer, keep-together); deck = a flip slide deck exported
one slide per landscape page (full-res screenshots → img2pdf, needs
pip install img2pdf). auto detects a deck by its #deck/.slide structure.
--template — the template folder name under templates/ (e.g. decarb,
rsra, portfolio-analysis, esg-fund-deck).
--data — the same JSON object you passed to fill_report (the render
payload). For a deck whose content is baked into the template, pass {}.
--title — optional; sets the cover/running-header title.
Example (verified): --template decarb --data templates/decarb/example-data.json
→ a 10-page US-Letter PDF (~290 KB), hero cover, running header + Page N of M
footer from page 2, the value-creation waterfall and cashflow tables intact.
The script prints a JSON result: {"ok": true, "out": …, "pages": N, "bytes": …}.
What it does (pipeline)
- Injects the data into the template's
<script id="report-data"> block and
serves it from a local HTTP server (templates load their own CDN-free assets).
- Runs it in headless Chromium so the template's
populateReport() fills the
DOM; waits for the window.__reportReady sentinel (fallback: a populated node).
- Applies a print transform: opens every
<details>, un-toggles chart
legend "off" states, derives the running header/footer from the report's own
title/meta, and relocates multi-state <select> views into a print appendix.
- Paginates with Paged.js consuming
assets/print.css (US-Letter @page,
@page:first full-bleed hero, running header via position: running(),
@bottom-* footer + counter(page)/counter(pages), break-inside: avoid
on cards/charts/figures, repeated thead on long tables, orphans/widows).
page.pdf(preferCSSPageSize, printBackground) → the final PDF.
Requirements
- Python
playwright + Chromium: python3 -m playwright install chromium
(the managed-agent worker image already ships this; on a bare box run it once).
Header / footer / overflow — how the spec is met
- Big header, page 1 only:
@page:first drops the top margin and hides the
running header so the template's own cover/hero bleeds to the top edge.
- Minimized header, page 2+: the report title is emitted as a
position: running() element shown in the @top-left/@top-right margin box.
- Footer everywhere: confidentiality/meta at
@bottom-left, Page N of M
at @bottom-right.
- Never overflow / split:
break-inside: avoid on sections, KPI tiles,
charts, figures, and metric rows; tables taller than a page split with their
header repeated; headings use break-after: avoid.
Notes & caveats
- Keep-together vs. whitespace: because charts/cards never split, a block
that doesn't fit in the remaining space moves to the next page, which can
leave whitespace above it (a section heading may sit a little above its chart).
This is the correct trade-off for "never overflow" — the content is intact on
the following page, never clipped.
- Charts must be SVG/DOM at populate time. SVG/HTML charts (the Soapbox
templates' default) render fine. A
<canvas> or a chart drawn on
IntersectionObserver/scroll may be blank in print — force it to draw during
populateReport() (don't lazy-render), or extend --timeout / the settle wait.
- Missing glyphs: icon-font glyphs the print fonts lack render as tofu (□).
Prefer inline SVG icons in templates destined for PDF.
- Fonts: templates link the Google Fonts they use; the PDF renderer has
network, so webfonts load (unlike the CSP-sandboxed in-app artifact viewer).
1---2name: report-pdf3description: Export any Soapbox paged HTML report/deck template to a consulting-grade, print-paginated PDF. Use when the user asks to download / export / save a report or deck as PDF, wants a "PDF version", a print-ready or shareable file, or a leave-behind. Works across every template that renders from a <script id="report-data"> payload (rsra, decarb, portfolio-analysis, esg-profile/esg-fund-deck, crrem, retrofit, delivery-presentation, …). Produces US-Letter pages with a big cover header on page 1, a minimized running header on pages 2+, a footer with page numbers, and keep-together pagination so components never split or overflow.4---56# report-pdf — paged HTML → consulting-grade PDF78Turns a Soapbox report/deck template (client-JS-populated HTML) into a paginated9US-Letter PDF with proper print vocabulary: a **full-bleed cover header on page101**, a **discreet running header on every subsequent page**, a **footer with11`Page N of M`**, and **keep-together rules so tables/charts/cards never split12across a page break or overflow the sheet**.1314## When to use15- "Export / download / save this report as a PDF", "PDF version", "print-ready",16 "leave-behind", "send me the deck as a file".17- After any report render (`fill_report`) when the user wants the PDF, not the18 interactive artifact.1920Do **not** use it to author a new template — that's `soapbox-report`. This skill21only paginates an existing template's rendered output.2223## How to run2425```bash26python3 scripts/export_pdf.py \27 --template <name> \28 --data <path/to/report-data.json> \29 --out <output.pdf> \30 [--title "Client — Report Title"] \31 [--mode auto|report|deck] \32 [--templates-dir <dir>] [--assets-dir <dir>] [--timeout 45000]33```3435`--mode` (default `auto`): **report** = continuous doc via Paged.js (hero page 1,36running header 2+, footer, keep-together); **deck** = a flip slide deck exported37one slide per landscape page (full-res screenshots → `img2pdf`, needs38`pip install img2pdf`). `auto` detects a deck by its `#deck`/`.slide` structure.3940- `--template` — the template folder name under `templates/` (e.g. `decarb`,41 `rsra`, `portfolio-analysis`, `esg-fund-deck`).42- `--data` — the same JSON object you passed to `fill_report` (the render43 payload). For a deck whose content is baked into the template, pass `{}`.44- `--title` — optional; sets the cover/running-header title.4546Example (verified): `--template decarb --data templates/decarb/example-data.json`47→ a 10-page US-Letter PDF (~290 KB), hero cover, running header + `Page N of M`48footer from page 2, the value-creation waterfall and cashflow tables intact.4950The script prints a JSON result: `{"ok": true, "out": …, "pages": N, "bytes": …}`.5152## What it does (pipeline)531. Injects the data into the template's `<script id="report-data">` block and54 serves it from a local HTTP server (templates load their own CDN-free assets).552. Runs it in headless Chromium so the template's `populateReport()` fills the56 DOM; waits for the `window.__reportReady` sentinel (fallback: a populated node).573. Applies a **print transform**: opens every `<details>`, un-toggles chart58 legend "off" states, derives the running header/footer from the report's own59 title/meta, and relocates multi-state `<select>` views into a print appendix.604. Paginates with **Paged.js** consuming `assets/print.css` (US-Letter `@page`,61 `@page:first` full-bleed hero, running header via `position: running()`,62 `@bottom-*` footer + `counter(page)/counter(pages)`, `break-inside: avoid`63 on cards/charts/figures, repeated `thead` on long tables, `orphans/widows`).645. `page.pdf(preferCSSPageSize, printBackground)` → the final PDF.6566## Requirements67- Python `playwright` + Chromium: `python3 -m playwright install chromium`68 (the managed-agent worker image already ships this; on a bare box run it once).6970## Header / footer / overflow — how the spec is met71- **Big header, page 1 only:** `@page:first` drops the top margin and hides the72 running header so the template's own cover/hero bleeds to the top edge.73- **Minimized header, page 2+:** the report title is emitted as a74 `position: running()` element shown in the `@top-left`/`@top-right` margin box.75- **Footer everywhere:** confidentiality/meta at `@bottom-left`, `Page N of M`76 at `@bottom-right`.77- **Never overflow / split:** `break-inside: avoid` on sections, KPI tiles,78 charts, figures, and metric rows; tables taller than a page split *with their79 header repeated*; headings use `break-after: avoid`.8081## Notes & caveats82- **Keep-together vs. whitespace:** because charts/cards never split, a block83 that doesn't fit in the remaining space moves to the next page, which can84 leave whitespace above it (a section heading may sit a little above its chart).85 This is the correct trade-off for "never overflow" — the content is intact on86 the following page, never clipped.87- **Charts must be SVG/DOM at populate time.** SVG/HTML charts (the Soapbox88 templates' default) render fine. A `<canvas>` or a chart drawn on89 `IntersectionObserver`/scroll may be blank in print — force it to draw during90 `populateReport()` (don't lazy-render), or extend `--timeout` / the settle wait.91- **Missing glyphs:** icon-font glyphs the print fonts lack render as tofu (□).92 Prefer inline SVG icons in templates destined for PDF.93- **Fonts:** templates link the Google Fonts they use; the PDF renderer has94 network, so webfonts load (unlike the CSP-sandboxed in-app artifact viewer).