HTML to PDF — Print-Quality Export
Export HTML pages to high-quality, print-ready vector PDFs using Playwright's page.pdf() API.
When to Use
Trigger with /html-to-pdf or when the user asks to:
- Export an HTML page to PDF
- Create a print-ready PDF from HTML
- Generate a PDF of a handout, card, flyer, or any HTML design
Critical Rules
NEVER screenshot-to-PDF. Never render HTML as a screenshot (PNG) and convert to PDF. This rasterizes text into pixels — unacceptable for print. Always use Playwright's
page.pdf()which produces vector PDFs with selectable, scalable text.Always emulate screen media. Chromium's PDF renderer defaults to
printmedia which washes out colors (dark backgrounds turn grey, accents look dull). Forcescreenmedia to preserve exact browser colors:await page.emulateMedia({ media: 'screen', colorScheme: 'dark' });Always set
printBackground: true. Without this, backgrounds are stripped entirely.RGB output only. Generate RGB PDFs. the user handles CMYK conversion in Adobe Acrobat with proper ICC profiles. Pillow's RGB→CMYK conversion produces off colors — never attempt it programmatically.
How It Works
The skill uses Playwright (Node.js) via a temporary script to:
- Launch headless Chromium
- Set viewport to match the HTML page dimensions
- Emulate screen media + dark color scheme for accurate color rendering
- Navigate to the local HTML file
- Export as PDF with exact page dimensions, zero margins, backgrounds enabled
Page Size — Pixels vs. Physical Units
Critical: Playwright's page.pdf() interprets pixel dimensions at 96 DPI. If your HTML body is 1056×1632px and you pass width: '1056px', the PDF page will be 11"×17" (1056÷96 = 11).
To get the correct physical size, you need two things:
- Physical units (
in,mm,cm) inpage.pdf()for the page dimensions scalefactor to shrink the viewport content to fit the smaller page
The viewport renders at the full HTML pixel size (e.g. 1056×1632). Without scale, the content overflows the smaller physical page. The scale factor = physical size ÷ (pixel size / 96).
Example: HTML body is 1056×1632px, target is 5.5"×8.5" at 192 DPI:
- PDF page at 96 DPI = 528×816 CSS px, but viewport is 1056×1632
- Scale = 528/1056 = 0.5 (equivalently: target DPI 96 ÷ design DPI 192 = 0.5)
- Result:
width: '5.5in', height: '8.5in', scale: 0.5
Quick formula: scale = 96 / designDPI. For 192 DPI → 0.5. For 144 DPI → 0.667. For 96 DPI → 1.0 (no scaling needed).
Export Script
Create this script at /tmp/pdf-export.mjs and run it with cd /tmp && node pdf-export.mjs:
import { chromium } from 'playwright-core';
// ── Configure these per export ──
const exports = [
// { name: 'descriptive-name', file: 'filename-without-extension' },
];
const baseDir = './marketing/events'; // adjust per job
const pageWidth = 1056; // match the HTML body width (px)
const pageHeight = 1632; // match the HTML body height (px)
const pdfWidth = '5.5in'; // physical page width — use in/mm/cm, NOT px
const pdfHeight = '8.5in'; // physical page height — use in/mm/cm, NOT px
// ─────────────────────────────────
const browser = await chromium.launch();
for (const p of exports) {
const context = await browser.newContext({
viewport: { width: pageWidth, height: pageHeight },
colorScheme: 'dark',
forcedColors: 'none',
});
const page = await context.newPage();
// Force screen media — critical for accurate colors
await page.emulateMedia({ media: 'screen', colorScheme: 'dark' });
await page.goto(`file://${baseDir}/${p.file}.html`, { waitUntil: 'networkidle' });
await page.pdf({
path: `${baseDir}/${p.file}.pdf`,
width: pdfWidth,
height: pdfHeight,
scale: 0.5, // 96 / designDPI — adjust if DPI changes
printBackground: true,
preferCSSPageSize: false,
margin: { top: '0', right: '0', bottom: '0', left: '0' },
});
console.log(`${p.name}: PDF saved → ${p.file}.pdf`);
await context.close();
}
await browser.close();
Step-by-Step Workflow
Read the HTML file to get the page dimensions from the CSS (
body { width: Xpx; height: Ypx; }).Create the export script at
/tmp/pdf-export.mjswith the correct:exportsarray (one entry per HTML file)baseDirpointing to the folder containing the HTML filespageWidthandpageHeightmatching the HTML body dimensions
Ensure playwright-core is installed:
cd /tmp && npm list playwright-core 2>/dev/null || npm install playwright-coreRun the export:
cd /tmp && node pdf-export.mjsOpen the PDF for review:
open /path/to/output.pdfVerify quality: Colors should match the browser exactly. Text should be selectable and crisp at any zoom level. Dark backgrounds should be true black, not washed-out grey.
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Washed-out / grey backgrounds | Print media default | Add page.emulateMedia({ media: 'screen' }) |
| No backgrounds at all | Missing flag | Add printBackground: true |
| PDF is wrong physical size (e.g. 11×17 instead of 5.5×8.5) | Using px units in page.pdf(), or missing scale |
Use physical units (in, mm) for width/height AND set scale: 96/designDPI (e.g. 0.5 for 192 DPI). Physical units alone cause clipping; scale alone leaves wrong page metadata. |
| Content clipped on right | Viewport too narrow | Match viewport.width to HTML body width exactly |
| Fonts not rendering | Google Fonts not loaded | Use waitUntil: 'networkidle' |
| Colors dull after printing | RGB→CMYK shift | Convert to CMYK in Adobe Acrobat with ICC profiles (not programmatic) |
Figma Handoff (Optional Post-Export Step)
After exporting to PDF, you can push the HTML design into Figma for team editing and visual refinement:
- Serve the HTML locally — The HTML file needs to be accessible via URL for Figma capture.
- Capture into Figma — Use
mcp__figma-remote-mcp__generate_figma_designto import the rendered page as a Figma design. - Choose output mode:
newFile— creates a fresh Figma file (good for new documents)existingFile— adds to an existing Figma file (good for adding pages to an ongoing project)
What this gives you: The design imports as a visual snapshot that the team can annotate, adjust typography, reposition elements, or polish in Figma's editor.
What it doesn't give you: Fully editable Figma components with proper text layers. The capture is a high-fidelity visual import, not a native Figma rebuild. For most whitepaper/datasheet workflows, this is the right trade-off: the HTML/CSS file remains the source of truth for content and layout, while Figma handles collaborative visual refinement.
When to use: Whitepapers, datasheets, one-pagers, or any multi-page branded document where the team wants to review or refine the design collaboratively in Figma before final delivery.
Output Location
Output saves alongside the source file.
Dependencies
- playwright-core (npm) — installed locally in
/tmp/node_modules/ - No global install needed;
cd /tmp && npm install playwright-coreis sufficient - Uses system Chromium bundled with Playwright