HTML → PDF (Playwright pipeline)
Render print-ready PDFs from HTML/CSS via Playwright (Chromium). Use this for business cards, certificates, invoices, postcards, letterhead, one-pagers — anything that ends up as a printable PDF. The pipeline is template-driven so the same renderer produces minimal black-and-white cards, editorial watercolor cards, or any other style added later. For PDF post-processing (imposition, crop marks, front+back merging), chain into Skill(document-skills:pdf).
When to use
Trigger when the user asks for:
- "design a business card", "print business cards", "redesign Luke's card"
- "render HTML as PDF", "set up an HTML print pipeline"
- "make a printable PDF", "generate a one-pager / postcard / certificate"
- Anything involving bleed, safe area, web fonts in print, or QR codes on cards
Layout
html-to-pdf/
├── SKILL.md
├── README.md
├── scripts/
│ ├── render.ts # Playwright renderer with template/style/theme selection
│ └── qr-artistic.ts # Artistic QR generator (round dots, finder eyes, logo overlay)
├── templates/
│ └── business-cards/
│ ├── employees/ # Per-person JSON + photos
│ │ ├── example.json
│ │ └── README.md
│ ├── minimal/ # B/W editorial style — single theme
│ │ ├── card.html
│ │ └── card-back.html
│ └── watercolor/ # Editorial style with pixel-photo + watercolor back
│ ├── assets/ # Background images for both themes
│ │ ├── sky.png
│ │ └── mountains.png
│ ├── light/
│ │ ├── card.html
│ │ └── card-back.html
│ └── dark/
│ ├── card.html
│ └── card-back.html
└── references/
├── print-rules.md # Bleed, font embed, color-profile gotchas
└── creating-a-template.md # How to add a new template / style / theme
Quick start
The renderer resolves all paths from the current working directory, so copy the skill files into a project root and run from there.
# Locate the skill — this varies by install method:
# - Plugin install: ls "$CLAUDE_PLUGIN_ROOT/skills/html-to-pdf"
# - Cloned repo: SKILL_ROOT="/path/to/prompts/skills/html-to-pdf"
# - npx skills add: ls .claude/skills/html-to-pdf
# Set SKILL_ROOT to whichever applies, then:
mkdir -p /tmp/print-job && cd /tmp/print-job
cp -R "$SKILL_ROOT/." .
# Install deps once
bun init -y
bun add playwright qrcode @types/qrcode geist @fontsource-variable/inter \
@fontsource-variable/fraunces bootstrap-icons
bunx playwright install chromium
Render a business card (always run from /tmp/print-job):
# Minimal style — no theme, no photo needed
bun scripts/render.ts --template business-cards --style minimal --employee example
# Watercolor / light — needs a portrait
bun scripts/render.ts --template business-cards --style watercolor --theme light \
--employee example --photo path/to/your-portrait.png
# Watercolor / dark — same template, same data, different theme
bun scripts/render.ts --template business-cards --style watercolor --theme dark \
--employee example --photo path/to/your-portrait.png
# Add a logo to the QR center (optional, any SVG or PNG)
bun scripts/render.ts --template business-cards --style minimal --employee example \
--logo path/to/your-logo.svg
Add a new employee by dropping employees/<slug>.json (use
employees/example.json as the schema) and passing --employee <slug>.
Output lands in out/<slug>-<style>[-<theme>]-{front,back}.pdf.
Picking a style + theme
| Style | Theme | Tone | When to use |
|---|---|---|---|
minimal |
(none) | Editorial black-on-white, ample whitespace, blocky fade-out accent | Default. Reads as serious, professional, formal. Print shop friendly. |
watercolor |
light |
Cream/parchment surface, Fraunces serif name, pixel-art portrait stamped on the card, watercolor sky back | When the recipient needs to remember the card. Editorial, warmer. |
watercolor |
dark |
Slate-indigo surface, cyan accents, watercolor mountain night-scene back | Brand-matched to dark-mode bopen.io. Distinctive at crypto conferences. |
A new style is one directory. A new theme is one subdirectory inside a style. See references/creating-a-template.md.
The non-obvious rules
These are the gotchas that break silently if you skip them — full details in references/print-rules.md.
@pagesize +preferCSSPageSize: true— set the trim+bleed dimensions in BOTH the CSS@pagerule and Playwright'spage.pdf({ width, height, preferCSSPageSize: true }). WithoutpreferCSSPageSize, Playwright silently falls back to US Letter.-webkit-print-color-adjust: exact— without this, Chromium strips background colors and images from the PDF. Black-background card backs come out white.Always embed fonts via
@font-face— referencing onlyfont-family: 'Inter', system-uiwithout an@font-faceproduces a PDF using whateversystem-uiis on the build machine (SF Pro on macOS) embedded as Type 3 path glyphs. Different RIPs render Type 3 differently. Templates use__NODE_MODULES__/...placeholders so fonts always come from the installed npm package.Wait for
document.fonts.readybeforepage.pdf()— capturing before fonts apply produces a PDF with system fallback glyphs.Bleed — render at trim + 2×0.125 in. The template HTMLs do this; new templates must too.
Full bleed/safe-area table, ghostscript/CMYK conversion notes, and font-embed troubleshooting live in references/print-rules.md.
Path placeholders in template HTML
So templates aren't tied to a specific filesystem layout, the renderer substitutes two tokens at render time:
| Placeholder | Resolves to | Use for |
|---|---|---|
__NODE_MODULES__/... |
node_modules/... (relative to working dir) |
Web fonts, icon SVGs |
__ASSETS__/... |
templates/<template>/<style>/assets/... |
Style-specific background images |
The template HTML can be moved to any project layout — the renderer fills in the paths.
Data-field placeholders (employee data)
| Placeholder | From employee JSON | Notes |
|---|---|---|
__NAME__ |
name |
Display name |
__TITLE__ |
title |
Role / subtitle |
__EMAIL__ |
email |
HTML-escaped |
__PHONE__ |
phone |
Optional |
__HANDLE__ |
handle |
Optional, rendered with X glyph |
__PHOTO_SRC__ |
--photo <path> CLI arg |
Required for templates that show a portrait |
__QR_SVG__ |
Generated from qrUrl |
Artistic QR with optional logo overlay |
__QR_LABEL__ |
qrLabel (defaults to qrUrl) |
Display text below the QR |
Every employee data field is HTML-escaped in the substitution step so a hostile name field can't inject script into the rendered page.
Known limitation: the substitution map in scripts/render.ts is currently hard-coded to the business-card field set. A new template that introduces fields like __EVENT__ or __VENUE__ requires extending the renderer. The clean refactor — looping the JSON object and substituting __<KEY-UPPERCASED>__ for each value — is deferred until a second non-card template lands. See references/creating-a-template.md.
Artistic QR codes
The bundled scripts/qr-artistic.ts generator builds the QR SVG by hand from the matrix returned by qrcode.create(). It supports:
- Round, rounded-square, or square data modules
- Custom rounded-square "eye" finder patterns
- A centered logo overlay (uses error-correction level H — up to ~22% of the QR width is recoverable)
- Configurable quiet zone (for cards where the QR sits directly on a non-uniform surface like a watercolor)
Always test the rendered QR by scanning the PDF with a phone camera before sending to print. Round-dot QRs with logo overlays can occasionally fail on cheap scanners even at level H.
Chaining with document-skills:pdf
After Playwright produces the per-page PDFs, invoke Skill(document-skills:pdf) for things HTML/CSS can't do:
- Merge front + back into a single 2-page deliverable
- 10-up imposition on US Letter for in-house cutting
- Crop marks / registration marks
- Embed PDF metadata (Title, Author, Producer)
- Compression / image downsampling
Adding new templates, styles, or themes
A new template is one directory. A new style under an existing template is one subdirectory. A new theme is one subdirectory inside a style.
templates/
└── <template>/ # e.g. business-cards, postcards, certificates
└── <style>/ # e.g. minimal, watercolor, brutalist
├── card.html # if single-theme
├── card-back.html
└── <theme>/ # if multi-theme
├── card.html
└── card-back.html
The renderer auto-discovers from the directory structure. See references/creating-a-template.md for a step-by-step walkthrough including the file fields each template HTML needs.
Provenance
This skill was extracted from the print pipeline behind bopen.io's business cards, where each teammate's QR resolves through a per-card vanity URL into the site's booking flow with attribution. The templates and gotchas reflect what actually held up at a commercial print shop.