agentleFS
educlopezeduclopez/mallard/skills/paginated-report-pdf/SKILL.md

paginated-report-pdf

Skill4 starsChanged 14 days agoMITView on GitHub

Generates a client-facing, document-style paginated PDF report (cover page, numbered sections, one page per topic/site/item, tables, callout boxes, numbered step lists) from an HTML source, using headless Chrome — no external PDF library needed. Use this whenever the user asks for a "PDF report", "informe en PDF", "documento para el cliente", "PDF con nuestra marca/logo", or wants an existing HTML dashboard/artifact turned into a proper paginated document instead of a browser screenshot or a raw print of the dashboard itself. Also use when asked to add a persistent logo/header to every page of a PDF, or to regenerate/update a PDF that was built this way before. Works for ANY brand/client — always run the brand intake step first, never assume a specific client's colors or logo.

The file

Show its rules and text

We split this skill into the rules it gives an agent

15 rules. About 0 are common, 6 are distinctive, and 9 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 9

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use this: when the deliverable is a real *document*: cover page, sections, tables, a page per item, meant to be read start-to-finish or …

    line 31
  2. Distinctive:

    Never reach for a heavier toolchain (wkhtmltopdf, Puppeteer, a PDF library) unless headless Chrome is confirmed unavailable — it almost …

    line 39
  3. Distinctive:

    Before writing any HTML, establish the brand's visual identity.

    line 47
  4. Very specific:

    Ask: If neither of the above resolves it, ask (a single AskUserQuestion covering all of the below, or a plain question if that tool isn't …

    line 57
  5. Distinctive:

    Default to a neutral dark gray/near-black if the user has no preference and there's truly no brand to match.

    line 62
  6. Very specific:

    Never hardcode a specific client's brand (colors, logo, name) as a default — every value in the HTML below is a placeholder resolved from …

    line 70
  7. Very specific:

    If missing, look for chromium/google-chrome on PATH, or ask the user.

    line 80
  8. Distinctive:

    Don't install a new dependency for this.

    line 81
  9. Very specific:

    Always include both -webkit-print-color-adjust: exact.

    line 133
  10. Very specific:

    If the user said "no logo", skip this step entirely and drop the <img> from the header template in Step 3 — don't fabricate a placeholder …

    line 145
  11. Very specific:

    Add this <div class="page-header"> as the first child of every <section class="page"> except the cover (which usually has its own full-size …

    line 184
  12. Very specific:

    Read a few pages of the generated PDF back (the Read tool renders PDF pages as images) — check: logo present per page (if one was …

    line 212
  13. Very specific:

    When inserting a new page in the middle of a numbered sequence (e.g.

    line 223
  14. Very specific:

    When batch-inserting the same snippet (like the per-page header in Step 3) into many <section class="page"> blocks at once, do it with a …

    line 228
  15. Distinctive:

    If this PDF is a paginated version of an existing interactive HTML dashboard, the two will drift if only one gets edited.

    line 257

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Paginated report PDF (HTML → Chrome headless → PDF)

Produces a real, linear document (cover, numbered sections, per-item pages) rendered by headless Chrome from a self-contained HTML file — not a screenshot, not a raw print of an interactive dashboard. Brand-agnostic: the skill asks for the brand's visual identity before writing a single line of HTML, so the same instructions produce a correctly-branded PDF for whichever client/project you're in.

When to use this vs. other options

  • Use this when the deliverable is a real document: cover page, sections, tables, a page per item, meant to be read start-to-finish or archived/emailed as a PDF.
  • Don't use this to just "export" an existing interactive dashboard (tabs, sidebar nav) as-is — that produces a broken, unreadable PDF (nav elements repeated, tab content hidden). Write a separate, purpose-built paginated HTML file instead. It's fine — even expected — for this file to reuse content/wording from the dashboard, restructured into linear document flow.
  • Never reach for a heavier toolchain (wkhtmltopdf, Puppeteer, a PDF library) unless headless Chrome is confirmed unavailable — it almost always already is (see prerequisite check below). If the task is PDF manipulation (merge/split/extract/forms/OCR) rather than authoring a new document, that's a different job — don't force this skill onto it.

Step 0 — brand intake (always do this first)

Before writing any HTML, establish the brand's visual identity. Resolve each of these, in order of preference:

  1. Already evident from the current project — an existing design system, theme.css/tokens file, a previous PDF built with this skill in the same repo, or a logo file already in the project (assets/, public/, static/). If found, reuse it and say so — don't re-ask for what's already on disk.
  2. Given directly by the user in this conversation — a color, a font name, a path/URL to a logo. Use it as stated.
  3. Ask. If neither of the above resolves it, ask (a single AskUserQuestion covering all of the below, or a plain question if that tool isn't available):
    • Logo: file path, URL, or "no logo for this one".
    • Primary color (hex) — used for callout borders, pills, accent text. Default to a neutral dark gray/near-black if the user has no preference and there's truly no brand to match.
    • Font family — a real font name if the brand has one; otherwise the system-font stack in Step 1 is a fine default, say so explicitly instead of silently picking something.
    • Document title / client name — goes in the cover page and the per-page header.

Never hardcode a specific client's brand (colors, logo, name) as a default — every value in the HTML below is a placeholder resolved from this step: {brand_name}, {primary_color}, {font_family}, {logo_data_uri}.

Prerequisite check

ls "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"

If missing, look for chromium/google-chrome on PATH, or ask the user. Don't install a new dependency for this.

Step 1 — write the paginated HTML source

One .html file, self-contained (no external CSS/font/image URLs — everything inline or base64, since the file may be printed with no network). Structure:

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>{brand_name} — {document_title}</title>
<style>
  :root { --brand-primary: {primary_color}; }
  @page { size: A4; margin: 22mm 18mm 20mm 18mm; }
  * { box-sizing: border-box; }
  body {
    font-family: {font_family}, -apple-system, "Helvetica Neue", Helvetica, Arial, sans-serif;
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }
  .page { page-break-after: always; }
  .page:last-child { page-break-after: auto; }
</style>
</head>
<body>

<section class="page"> ... cover ... </section>
<section class="page"> ... section 2 ... </section>
<!-- one <section class="page"> per logical page; content that overflows a page
     just flows onto the next physical page automatically, no extra markup needed -->

</body>
</html>

Key CSS pieces worth building (reference tokens like var(--brand-primary), never a hardcoded hex, so the same structure repaints for any brand):

  • .callout / .callout.strong — colored left-border box for a key finding or warning, border color = var(--brand-primary).
  • .stat-grid / .stat .num — big-number summary stats row.
  • .pill.good / .pill.warn — small colored status badges inside tables.
  • .steps li + .step-circle — numbered/checkmarked circular steps (a plan or a checklist), circle background = var(--brand-primary).
  • .kv-table — a label/value table (first column bold+uppercase+narrow) for a spec sheet.
  • .item-badge, .item-head, .meta-pills — a per-item detail-page header pattern (name, URL/ref, status badge, spec chips) — reuse this whenever the report has "one page per X" (per site, per store, per module, per incident).

Always include both -webkit-print-color-adjust: exact; and print-color-adjust: exact; on body — without them Chrome's print engine can wash out background colors on callout boxes.

Step 2 — embed the logo (or any image) as base64, once

curl -sL -o /tmp/logo.png "<logo-path-or-url-from-step-0>"
base64 -i /tmp/logo.png | tr -d '\n' > /tmp/logo.b64

Then inline it: <img src="data:image/png;base64,$(cat /tmp/logo.b64)" alt="{brand_name}">. If the user said "no logo", skip this step entirely and drop the <img> from the header template in Step 3 — don't fabricate a placeholder logo.

Once embedded in one file, extract it back out for reuse elsewhere with:

grep -o 'data:image/png;base64,[A-Za-z0-9+/=]*' source.html | head -1

— no need to re-fetch/re-encode the image for every file that needs it, or for a later PDF for the same brand.

Step 3 — logo/header on every single page (not just the cover)

Chrome's CLI (--headless --print-to-pdf) does not support a custom per-page header/footer template (that's a Puppeteer-only API, page.pdf({headerTemplate})) — --print-header-footer only gives Chrome's own plain URL/date/title footer, not a custom image. The reliable fix: put the logo in the HTML content of every page section, not in a browser-managed header:

.page-header {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 18px; padding-bottom: 10px; border-bottom: 1px solid #eeeef1;
}
.page-header img { height: 14px; width: auto; }
<section class="page">
  <div class="page-header">
    <img src="data:image/png;base64,..." alt="{brand_name}">
    <span class="doc-title">{document_title}</span>
  </div>
  ... rest of page content ...
</section>

Add this <div class="page-header"> as the first child of every <section class="page"> except the cover (which usually has its own full-size logo treatment already). If there's no logo (Step 0/2 said so), keep just the <span class="doc-title">. If there are many pages, script the insertion rather than hand-editing each one — see the one-liner pattern below.

Step 4 — generate the PDF

"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --headless --disable-gpu --no-pdf-header-footer \
  --print-to-pdf="/absolute/path/to/output.pdf" \
  "file:///absolute/path/to/source.html"
  • --no-pdf-header-footer suppresses Chrome's own plain-text header/footer (URL, date, page number) — always pass it, since the HTML already carries its own header per Step 3.
  • The file:// URL must be an absolute path.
  • Chrome prints a lot of unrelated updater/crashpad log noise to stderr — harmless, ignore it; check the actual output file size/existence instead of parsing stderr for success.
  • This runs Chrome fully headless, no window, no user interaction — safe to run repeatedly while iterating.

Step 5 — verify before calling it done

Read a few pages of the generated PDF back (the Read tool renders PDF pages as images) — check: logo present per page (if one was requested), tables didn't get cut mid-row across a page break in an ugly way, colored boxes rendered with the brand color (not washed out or defaulted to a generic color), no leftover placeholder text ({brand_name} etc. never survives into the actual output — if you see it, Step 0 was skipped or a substitution was missed). Iterate on the HTML and regenerate — this loop is fast (a few seconds per Chrome invocation).

Iterating on structure (adding/removing pages, renumbering sections)

When inserting a new page in the middle of a numbered sequence (e.g. a "2 · Section name" eyebrow scheme), grep for all section-eyebrow occurrences first and fix the numbering in one pass — it's easy to insert a new page and forget every later page still says the old number.

When batch-inserting the same snippet (like the per-page header in Step 3) into many <section class="page"> blocks at once, do it with a small Python script that splits on <section class="page and inserts right after each opening tag's > — faster and less error-prone than editing each section by hand:

import re
with open(path, encoding='utf-8') as f:
    c = f.read()
m = re.search(r'data:image/png;base64,[A-Za-z0-9+/=]+', c)
data_uri = m.group(0) if m else None  # None if this brand has no logo
header_html = (
    f'\n  <div class="page-header"><img src="{data_uri}" alt="{{brand_name}}" /><span class="doc-title">{{document_title}}</span></div>'
    if data_uri else
    f'\n  <div class="page-header"><span class="doc-title">{{document_title}}</span></div>'
)
parts = c.split('<section class="page')
new_parts = [parts[0], parts[1]]  # keep head + cover page untouched
for seg in parts[2:]:
    idx = seg.index('>') + 1
    new_parts.append(seg[:idx] + header_html + seg[idx:])
c = '<section class="page'.join(new_parts)
with open(path, 'w', encoding='utf-8') as f:
    f.write(c)

Keeping the PDF in sync with other artifacts

If this PDF is a paginated version of an existing interactive HTML dashboard, the two will drift if only one gets edited. When a fact changes: update the dashboard's per-item section AND the matching PDF-source page AND regenerate the PDF in the same pass — don't defer the PDF regen "for later", it's cheap (Step 4 is one command) and skipping it is exactly how documents go stale.

Prior art

This skill's technique (HTML → Chrome headless → PDF, per-page HTML header since Chrome CLI has no custom header template) was worked out end-to-end on a real client deliverable — a security-audit report with a cover page, per-site incident pages, callout boxes, and a persistent logo header. That file is a legitimate reference for the CSS patterns in Step 1 if you want to see them fully worked, but treat every brand value in it as project-specific — never copy its actual colors/logo into a different client's report.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.