kagen · 紙源
紙源 · かげん - paper source. PDF generation companion to Kami.
Kami designs, Kagen ships. Converts Kami HTML templates to production-grade PDF using Chromium (Playwright), bypassing WeasyPrint's Windows limitations.
Why Kagen
| Problem |
Solution |
| WeasyPrint doesn't work on Windows (no GTK) |
Kagen uses Playwright/Chromium — works everywhere |
| WeasyPrint cold-start ~630ms per render |
Playwright warm ~13ms per render |
| WeasyPrint can't execute JS |
Chromium renders fully (charts, dynamic content) |
| wkhtmltopdf is deprecated and unmaintained |
Playwright is actively maintained by Microsoft |
Based on benchmarks (pdf4.dev 2026), engineering discussions (HN, Stack Overflow, BrowserStack), and production experience from DocRaptor, customjs.space, and print-css.rocks.
Prerequisites
- Node.js 20+
- Playwright (
npx playwright auto-installs on first use)
- Chromium browser installed (
npx playwright install chromium)
- Kami HTML templates (any valid HTML with print CSS)
Quick start
npx playwright pdf "file:///path/to/doc.html" "output.pdf"
Or from Node.js:
const { chromium } = require('playwright');
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('file:///path/to/doc.html', { waitUntil: 'networkidle' });
await page.pdf({
path: 'output.pdf',
format: 'A4',
printBackground: true,
margin: { top: '0', bottom: '0', left: '0', right: '0' }
});
await browser.close();
Production settings (from professional research)
Page options
await page.pdf({
path: 'output.pdf',
format: 'A4', // or 'Letter', 'A3'
printBackground: true, // always true — renders parchment bg
margin: { top: '0', bottom: '0', left: '0', right: '0' },
// For screen media (not print):
// await page.emulateMedia({ media: 'screen' });
});
Critical CSS rules for reliable PDF output
/* Always include in your HTML template header */
@page {
size: A4;
margin: 24mm 26mm 26mm 26mm;
background: #f5f4ed;
widows: 4;
orphans: 4;
}
/* Force background colors in Chromium */
* {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* Avoid orphan text lines — high widows/orphans prevents single-line splits */
body { widows: 4; orphans: 4; }
p { widows: 3; orphans: 3; }
li { widows: 2; orphans: 2; }
/* Page break control — only on chapters with heavy content */
.chapter { }
.chapter.break { break-before: page; }
/* Never let a heading sit alone at page bottom */
h1, h2, h3, h4 { break-after: avoid; }
/* Keep these blocks intact — never split across pages */
table, pre, figure, .callout, .card,
blockquote, .finding-header, .takeaway {
break-inside: avoid;
}
/* Avoid code blocks getting orphaned from their preceding paragraph */
pre {
margin-top: 6pt;
page-break-before: avoid;
}
/* Tables should not break rows across pages */
table tr {
break-inside: avoid;
}
Visual patterns by document type
Each document type needs its own set of visual patterns. Apply accordingly:
| Type |
Required patterns |
Optional patterns |
| Pentest / Security report |
Severity badges (red/orange/green), risk bar, code blocks with left border, impact/remediation boxes |
Finding-header with metadata, findings table with badges |
| One-pager / Executive summary |
Glance grid (4 metrics), lead paragraph, takeaway box, cover with large title + decorative line |
Callout for key data point, footer with contact |
| White paper / Long doc |
Chapter breaks in dense sections, table of contents, callouts, keep-together on critical blocks |
Quotes, diagrams, appendix |
| Letter |
Wide margins (25mm), formal greeting and closing, no columns, no tables |
Letterhead, signature |
| Resume |
Dense body (9.2pt), metric row, project bullets with action + result |
Timeline, skills grid |
| Slides |
Assertion-evidence titles, one idea per slide, one-line bullets, pinned callout |
Code cards, 2x2 table |
Rule: if the document type isn't in the table, choose the closest one and adapt.
Near-empty pages prevention
Nothing looks more amateur than a page with 2 lines. Causes and solutions:
| Cause |
Solution |
Auto-detection |
| Heading with 1 short paragraph at the end |
Merge with previous section |
If a chapter has only 1 h2 + 1 p, it doesn't deserve its own page |
break-before: page on every section |
Only use on chapters with >1/3 page of content |
Count paragraphs + tables + lists. If they add up to less than 5 elements, don't force a break |
break-inside: avoid on large block that doesn't fit |
Relax break-inside or split the block |
If a keep-together measures more than 1 page, don't force it |
| Source list at the end spilling onto a separate page |
Move to consolidated sources chapter |
Sources go in one place, not repeated in each chapter |
AI anti-patterns in documents
Validate that content inherited from Kami doesn't have these marks:
| Anti-pattern |
Problem |
Fix |
Repeated em dash — as label/value separator |
Black Box — no credentials, multiple lines in a row |
Parentheses, comma, colon. The dash is for genuine asides, not for separating labels |
| Uniform tables without color |
All rows identical, no visual indication of severity or priority |
Color badges, subtle zebra rows, highlighted first column |
| Code blocks without contrast |
They blend with the body, don't look like code |
Left blue border, ivory background, monospace, generous padding |
| Same structure on every page |
Every page is title + table or title + list |
Vary: finding box, risk bar, flowchart, callout. Alternate rhythm |
| Claims without source |
"LLMs hallucinate 15-20%" without attribution |
"According to Vectara HHEM 2026, LLMs..." |
Best practices from professionals
| Practice |
Source |
Why |
Always set printBackground: true |
BrowserStack, Playwright docs |
Kami uses parchment bg #f5f4ed — Chromium strips it by default |
Use file:// protocol |
Stack Overflow, DocuPotion |
Avoids auth/CORS issues with local files |
Set waitUntil: 'networkidle' |
BrowserStack |
Ensures fonts, CSS fully loaded |
| Lock browser version in CI |
BrowserStack, blog.rasc.ch |
Chromium updates can change rendering |
Use @page margins, not Playwright margins |
print-css.rocks, CSS Paged Media spec |
CSS margins are more predictable for paged media |
-webkit-print-color-adjust: exact |
MDN, Chromium docs |
Critical for parchment backgrounds and brand colors |
| Reuse browser instance (warm) |
pdf4.dev benchmark |
42ms → 3ms speedup (14x) |
| Validate PDF visually in CI |
BrowserStack |
Page count, whitespace, font check |
Warm mode (production pipeline)
const { chromium } = require('playwright');
class PDFRenderer {
constructor() {
this.browser = null;
}
async start() {
this.browser = await chromium.launch();
}
async render(htmlPath, outputPath) {
const page = await this.browser.newPage();
await page.goto('file:///' + htmlPath.replace(/\\/g, '/'), {
waitUntil: 'networkidle'
});
await page.pdf({
path: outputPath,
format: 'A4',
printBackground: true,
margin: { top: '0', bottom: '0', left: '0', right: '0' }
});
await page.close();
}
async stop() {
await this.browser.close();
}
}
Font embedding
Chromium embeds system fonts by default. For custom fonts (like TsangerJinKai02 in Kami):
@font-face {
font-family: "CustomFont";
src: url("fonts/CustomFont.woff2") format("woff2");
font-weight: 400;
font-style: normal;
}
Place font files relative to the HTML and use relative src paths. Chromium resolves them from the HTML file's directory.
Troubleshooting
| Problem |
Fix |
| White background instead of parchment |
Add printColorAdjust: exact in CSS and printBackground: true in JS |
| Fonts not rendering |
Use relative paths in @font-face, check font file exists |
| Page breaks wrong |
Check break-inside: avoid on tables, pre, callout |
| Near-empty page (2 lines alone) |
Merge short section with previous; avoid break-before: page on light chapters |
| Content overflow |
Use page-break-inside: avoid on large blocks |
| Chinese chars as boxes |
Include CJK font in @font-face or use system CJK fallback |
| PDF too large |
Remove unnecessary images, compress embedded fonts |
| Slow first render |
Keep browser instance alive (warm mode) |
CSS visual pattern snippets
Concrete patterns to include in the HTML by document type:
/* Severity badges (pentest, security) */
.badge-high { background: #f5e8e8; color: #a83030; display: inline-block; padding: 2pt 7pt; border-radius: 3pt; font-size: 8pt; font-weight: 500; text-transform: uppercase; }
.badge-medium { background: #f5ede2; color: #b86a25; display: inline-block; padding: 2pt 7pt; border-radius: 3pt; font-size: 8pt; font-weight: 500; text-transform: uppercase; }
.badge-ok { background: #e8f2ec; color: #2a7a4a; display: inline-block; padding: 2pt 7pt; border-radius: 3pt; font-size: 8pt; font-weight: 500; text-transform: uppercase; }
/* Risk bar (pentest exec summary) */
.risk-bar { display: flex; gap: 2pt; margin: 8pt 0 14pt 0; height: 12pt; }
.risk-bar .seg { border-radius: 2pt; display: flex; align-items: center; justify-content: center; font-size: 7pt; color: #fff; font-weight: 500; }
/* Code blocks with left border (pentest, technical) */
pre { border-left: 2.5pt solid #1B365D; border-radius: 3pt; background: #faf9f5; padding: 10pt 14pt; font-family: Consolas, monospace; font-size: 9pt; line-height: 1.5; break-inside: avoid; }
/* Impact/Remediation boxes (pentest) */
.impact-box, .remediation-box { border-left: 2pt solid #e8e6dc; padding-left: 12pt; margin: 10pt 0 14pt 0; break-inside: avoid; }
.impact-box .label, .remediation-box .label { font-size: 8.5pt; letter-spacing: 0.8pt; text-transform: uppercase; font-weight: 500; color: #1B365D; margin-bottom: 4pt; }
/* Cover accent line */
.cover-line { width: 60pt; height: 2pt; background: #1B365D; margin: 20pt 0; border-radius: 1pt; }
/* Pipeline flow (4 step horizontal) */
.pipeline-flow { display: flex; gap: 0; margin: 18pt 0; break-inside: avoid; }
.pipeline-step { flex: 1; text-align: center; padding: 10pt 8pt; }
.pipeline-step .dot { width: 6pt; height: 6pt; border-radius: 50%; background: #1B365D; margin: 0 auto 5pt auto; }
.pipeline-step + .pipeline-step { border-left: 0.5pt dotted #e8e6dc; }
.pipeline-step .name { font-size: 9pt; font-weight: 500; color: #141413; margin-bottom: 3pt; }
.pipeline-step .desc { font-size: 7.5pt; color: #6b6a64; line-height: 1.35; }
/* Keep-together wrapper */
.keep-together { break-inside: avoid; }
Self-review protocol (mandatory)
Don't generate the PDF without passing this checklist. Each item is a real failure documented in previous iterations.
Structural (blocking)
Visual (high priority)
Content (high priority)
Technical (blocking)
Complete HTML example
Minimum functional template that includes all essential patterns. Copy and adapt:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document Title</title>
<style>
@page { size: A4; margin: 24mm 26mm 26mm 26mm; background: #f5f4ed; widows: 4; orphans: 4; }
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-print-color-adjust: exact; }
:root {
--parchment: #f5f4ed; --ivory: #faf9f5; --near-black: #141413;
--dark-warm: #3d3d3a; --olive: #504e49; --stone: #6b6a64;
--brand: #1B365D; --border: #e8e6dc; --border-soft: #e5e3d8;
--serif: Charter, Georgia, Palatino, "Times New Roman", serif;
--mono: Consolas, "Courier New", monospace;
}
body { font-family: var(--serif); font-size: 10.5pt; line-height: 1.65; color: var(--near-black); background: var(--parchment); widows: 4; orphans: 4; }
h1 { font-size: 24pt; border-left: 2.5pt solid var(--brand); padding-left: 8pt; margin: 0 0 10pt 0; break-after: avoid; }
h2 { font-size: 16pt; margin: 24pt 0 8pt 0; break-after: avoid; }
p { margin: 0 0 10pt 0; widows: 3; orphans: 3; }
table { width: 100%; border-collapse: collapse; font-size: 9.5pt; margin: 12pt 0; break-inside: avoid; }
table th { text-align: left; padding: 6pt 8pt; border-bottom: 1pt solid var(--border); background: var(--ivory); }
table td { padding: 5pt 8pt; border-bottom: 0.3pt solid var(--border-soft); }
table tr { break-inside: avoid; }
pre { border-left: 2.5pt solid var(--brand); border-radius: 3pt; background: var(--ivory); padding: 10pt 14pt; font-size: 9pt; font-family: var(--mono); break-inside: avoid; margin: 6pt 0 10pt 0; }
.callout { background: var(--ivory); border-left: 2pt solid var(--brand); padding: 10pt 14pt; border-radius: 3pt; margin: 12pt 0; break-inside: avoid; }
.keep-together { break-inside: avoid; }
.break { break-before: page; }
/* Add visual patterns by document type (badges, risk bar, etc.) */
</style>
</head>
<body>
<!-- Cover -->
<section style="min-height:240mm;display:flex;flex-direction:column;justify-content:space-between;padding:40mm 0 0 0;break-after:page;">
<div>
<div style="font-size:10pt;color:var(--brand);letter-spacing:2pt;text-transform:uppercase;margin-bottom:18pt;">Document Type</div>
<div style="font-size:40pt;font-weight:500;line-height:1.12;margin-bottom:16pt;">Main Title</div>
<div style="width:60pt;height:2pt;background:var(--brand);margin:20pt 0;border-radius:1pt;"></div>
<div style="font-size:14pt;color:var(--olive);max-width:85%;">Subtitle or description</div>
</div>
<div style="font-size:10pt;color:var(--stone);">Author · Date</div>
</section>
<!-- Chapter -->
<section class="break">
<h1>Chapter Title</h1>
<p>Document content. Verify sources (research), humanize text (humanize), design with Kami.</p>
<!-- Tables, lists, callouts as needed -->
</section>
</body>
</html>
Integration with Kami
# One-liner from Kami HTML to PDF
npx playwright pdf "file:///path/to/kami-output.html" "final.pdf"
Sources
Workflow
Step 1: Validate HTML template
Before rendering, verify the template is production-ready:
- Open the HTML in a browser first: confirm fonts load, CSS applies, layout renders correctly at print size
- Check that
@page rules specify explicit size (A4, Letter) and margins
- Verify
-webkit-print-color-adjust: exact and print-color-adjust: exact are set on *
- Confirm all
@font-face declarations use relative paths, not absolute filesystem paths
Step 2: Configure render settings
- Set
format (A4, Letter, A3) to match the template's @page { size: } declaration
- Set
printBackground: true — always. Never skip this. Chromium strips backgrounds by default
- Set Playwright
margin: { top: '0', bottom: '0', left: '0', right: '0' } — use CSS @page margins instead
- Use
waitUntil: 'networkidle' to ensure fonts, CSS, and images are fully loaded before rendering
Step 3: Render PDF
- Launch browser once (warm mode) — reuse for all documents in batch
- Navigate with
file:// protocol to avoid CORS and authentication issues with local files
- Await
page.pdf() with configured options
- Close page after each render but keep browser alive for next document
Step 4: Validate output PDF
- Open PDF and check: parchment/tinted background rendered (not white), fonts embedded correctly, page breaks occur at logical points
- Scan every page for near-empty pages (2 lines or less floating at page top)
- Verify code blocks, callouts, and tables are not split across page boundaries
- Check CJK characters render correctly if document contains Chinese, Japanese, or Korean text
Step 5: Apply visual patterns by document type
- Pentest/security report: severity badges, risk bars, finding headers, styled code blocks, impact/remediation boxes
- One-pager/executive summary: glance grid (4 metrics), lead paragraph, takeaway box, cover with decorative accent line
- White paper/long document: chapter breaks on dense sections, callouts, keep-together wrappers on critical blocks
- Letter: wide margins (25mm), formal greeting/closing, single-column, no unnecessary breaks
- Resume: dense body (9.2pt), metric row, project bullets with action + result format
Step 6: Run self-review protocol
Complete ALL four checklist categories before final output: Structural (blocking), Visual (high priority), Content (high priority), Technical (blocking). Every item in the self-review protocol is a real failure mode documented from previous iterations.
Error Handling
| Cause |
Fix |
| PDF renders with stark white background instead of parchment (#f5f4ed) |
Chromium strips all backgrounds in print mode. Ensure BOTH printBackground: true in Playwright JS config AND -webkit-print-color-adjust: exact on * in CSS |
| Custom fonts render as fallback system serif in the PDF output |
Use relative paths in @font-face src: url("fonts/CustomFont.woff2"). Place font files in same directory as HTML or a subdirectory. Chromium resolves relative to the HTML file's location |
| CJK characters (Chinese, Japanese, Korean) render as empty boxes or tofu |
Include a CJK-capable font via @font-face or add system CJK fallback stack: font-family: "TsangerJinKai02", "SimSun", "MS Mincho", serif. Test before batch rendering |
| Near-empty page at end of section (2 lines of text floating alone) |
Merge the short section with the previous one. Only apply break-before: page to chapters with > 1/3 page of content. Count elements: if heading + paragraph count < 5 total, don't force a page break |
| Table rows split awkwardly across consecutive pages |
Add table tr { break-inside: avoid; } in CSS. For large spanning tables, add table { break-inside: avoid; } so the entire table moves as a block |
| Page break leaves an isolated heading at the very bottom of a page |
Add h1, h2, h3, h4 { break-after: avoid; } to ensure at least the first content element after a heading stays with it on the same page |
| PDF file size is excessive (10MB+ for a text-heavy document) |
Remove unnecessary raster images. Subset and compress embedded fonts to only used glyphs. Reduce DPI of any embedded raster content. Check for duplicated embedded resources |
page.pdf() call hangs indefinitely on networkidle with dynamic/JS-rendered content |
Switch to waitUntil: 'load' or add explicit page.waitForTimeout(3000). For charts and JS-rendered content, use waitForSelector() on a known rendered element |
| Chromium version mismatch between local dev and CI causes visual output differences |
Lock Chromium version explicitly: note the version npx playwright install chromium installs. Document it in CI config. Re-baseline visual checks on every Chromium version bump |
| Multiple documents rendered in bulk — browser crashes after N documents due to memory |
Implement page pooling: close and reopen pages every 10-20 documents. Or restart browser every 50 documents. Memory leak in Chromium PDF rendering is a known issue |
Anti-Patterns
| Pattern |
Problem |
Fix |
Setting margins via Playwright margin parameter instead of CSS @page |
Playwright margin rendering is inconsistent across Chromium versions. CSS @page margins produce predictable, standards-compliant output |
Always use @page { margin: 24mm 26mm 26mm 26mm; } in CSS. Set Playwright margin: { top: '0', bottom: '0', left: '0', right: '0' } |
break-before: page applied to every section/chapter unconditionally |
Creates near-empty pages when a section has little content. Reader sees a page with 2 lines. Looks unprofessional and amateur |
Only force page breaks on chapters with > 1/3 page of content. Count paragraph + table + list elements. If < 5 total content elements, skip the forced break |
| All pages share identical visual structure (title → table → title → table) |
Monotonous reading experience. Reader disengages after page 3. Document fails to sustain attention through key findings |
Vary rhythm: finding box, then risk bar, then callout, then table, then narrative paragraph. Alternate between data-dense and commentary pages |
| Repeated em dashes used as label/value separator in tables and lists |
"Black Box — no credentials" repeated 15 times in a pentest report. Jarring, lazy, reads like raw data dump |
Use colon separator (Black Box: no credentials), parentheses, or proper table columns with distinct header and value styling |
| Code blocks visually indistinguishable from surrounding body text |
Reader skims past code assuming it's prose. Reduces document credibility. Technical content loses all impact |
Style every code block: 2.5pt left border, ivory/tinted background, monospace font (Consolas/Courier), generous padding (10-14pt), slight border-radius |
printBackground: false anywhere in the production rendering pipeline |
Chrome strips all background colors, including parchment. Document renders on stark white default. Destroys the Kami visual design |
Always printBackground: true. Add CI assertion: if PDF renders with white background, fail the build |
| Launching a new browser instance per PDF document |
Cold start ~630ms vs warm ~13ms. 48x slower per document. Wastes CI minutes on multi-document projects |
Use warm mode (PDFRenderer class pattern): launch browser once, render all documents, close browser once. Keep-alive between renders |
Font files referenced with absolute Windows paths (C:\Users\swagger\fonts\...) |
PDF generated on a different machine or in CI won't find fonts. Paths break across environments. Not portable |
Always use relative paths in @font-face: src: url("fonts/CustomFont.woff2"). Place font files in same directory or subdirectory as the HTML template |
| Skipping the self-review protocol because "it looks fine in the browser" |
Browser rendering != PDF output. Chrome applies different print stylesheet rules. Many issues only visible in the final PDF |
Always open the actual PDF output and run through all 4 checklist categories. Visual inspection is mandatory, not optional |
Checklist
1---2name: kagen3description: kagen · 紙源4---56# kagen · 紙源78**紙源 · かげん** - paper source. PDF generation companion to Kami.910Kami designs, **Kagen ships**. Converts Kami HTML templates to production-grade PDF using Chromium (Playwright), bypassing WeasyPrint's Windows limitations.1112## Why Kagen1314| Problem | Solution |15|---------|----------|16| WeasyPrint doesn't work on Windows (no GTK) | Kagen uses Playwright/Chromium — works everywhere |17| WeasyPrint cold-start ~630ms per render | Playwright warm ~13ms per render |18| WeasyPrint can't execute JS | Chromium renders fully (charts, dynamic content) |19| wkhtmltopdf is deprecated and unmaintained | Playwright is actively maintained by Microsoft |2021Based on benchmarks (pdf4.dev 2026), engineering discussions (HN, Stack Overflow, BrowserStack), and production experience from DocRaptor, customjs.space, and print-css.rocks.2223## Prerequisites2425- Node.js 20+26- Playwright (`npx playwright` auto-installs on first use)27- Chromium browser installed (`npx playwright install chromium`)28- Kami HTML templates (any valid HTML with print CSS)2930## Quick start3132```powershell33npx playwright pdf "file:///path/to/doc.html" "output.pdf"34```3536Or from Node.js:3738```js39const { chromium } = require('playwright');40const browser = await chromium.launch();41const page = await browser.newPage();42await page.goto('file:///path/to/doc.html', { waitUntil: 'networkidle' });43await page.pdf({44 path: 'output.pdf',45 format: 'A4',46 printBackground: true,47 margin: { top: '0', bottom: '0', left: '0', right: '0' }48});49await browser.close();50```5152## Production settings (from professional research)5354### Page options5556```js57await page.pdf({58 path: 'output.pdf',59 format: 'A4', // or 'Letter', 'A3'60 printBackground: true, // always true — renders parchment bg61 margin: { top: '0', bottom: '0', left: '0', right: '0' },62 // For screen media (not print):63 // await page.emulateMedia({ media: 'screen' });64});65```6667### Critical CSS rules for reliable PDF output6869```css70/* Always include in your HTML template header */7172@page {73 size: A4;74 margin: 24mm 26mm 26mm 26mm;75 background: #f5f4ed;76 widows: 4;77 orphans: 4;78}7980/* Force background colors in Chromium */81* {82 -webkit-print-color-adjust: exact;83 print-color-adjust: exact;84}8586/* Avoid orphan text lines — high widows/orphans prevents single-line splits */87body { widows: 4; orphans: 4; }88p { widows: 3; orphans: 3; }89li { widows: 2; orphans: 2; }9091/* Page break control — only on chapters with heavy content */92.chapter { }93.chapter.break { break-before: page; }9495/* Never let a heading sit alone at page bottom */96h1, h2, h3, h4 { break-after: avoid; }9798/* Keep these blocks intact — never split across pages */99table, pre, figure, .callout, .card,100blockquote, .finding-header, .takeaway {101 break-inside: avoid;102}103104/* Avoid code blocks getting orphaned from their preceding paragraph */105pre {106 margin-top: 6pt;107 page-break-before: avoid;108}109110/* Tables should not break rows across pages */111table tr {112 break-inside: avoid;113}114```115116### Visual patterns by document type117118Each document type needs its own set of visual patterns. Apply accordingly:119120| Type | Required patterns | Optional patterns |121|------|----------------------|---------------------|122| **Pentest / Security report** | Severity badges (red/orange/green), risk bar, code blocks with left border, impact/remediation boxes | Finding-header with metadata, findings table with badges |123| **One-pager / Executive summary** | Glance grid (4 metrics), lead paragraph, takeaway box, cover with large title + decorative line | Callout for key data point, footer with contact |124| **White paper / Long doc** | Chapter breaks in dense sections, table of contents, callouts, keep-together on critical blocks | Quotes, diagrams, appendix |125| **Letter** | Wide margins (25mm), formal greeting and closing, no columns, no tables | Letterhead, signature |126| **Resume** | Dense body (9.2pt), metric row, project bullets with action + result | Timeline, skills grid |127| **Slides** | Assertion-evidence titles, one idea per slide, one-line bullets, pinned callout | Code cards, 2x2 table |128129**Rule:** if the document type isn't in the table, choose the closest one and adapt.130131### Near-empty pages prevention132133Nothing looks more amateur than a page with 2 lines. Causes and solutions:134135| Cause | Solution | Auto-detection |136|-------|----------|---------------------|137| Heading with 1 short paragraph at the end | Merge with previous section | If a chapter has only 1 h2 + 1 p, it doesn't deserve its own page |138| `break-before: page` on every section | Only use on chapters with >1/3 page of content | Count paragraphs + tables + lists. If they add up to less than 5 elements, don't force a break |139| `break-inside: avoid` on large block that doesn't fit | Relax `break-inside` or split the block | If a keep-together measures more than 1 page, don't force it |140| Source list at the end spilling onto a separate page | Move to consolidated sources chapter | Sources go in one place, not repeated in each chapter |141142### AI anti-patterns in documents143144Validate that content inherited from Kami doesn't have these marks:145146| Anti-pattern | Problem | Fix |147|-------------|----------|-----|148| Repeated em dash `—` as label/value separator | `Black Box — no credentials`, multiple lines in a row | Parentheses, comma, colon. The dash is for genuine asides, not for separating labels |149| Uniform tables without color | All rows identical, no visual indication of severity or priority | Color badges, subtle zebra rows, highlighted first column |150| Code blocks without contrast | They blend with the body, don't look like code | Left blue border, ivory background, monospace, generous padding |151| Same structure on every page | Every page is title + table or title + list | Vary: finding box, risk bar, flowchart, callout. Alternate rhythm |152| Claims without source | "LLMs hallucinate 15-20%" without attribution | "According to Vectara HHEM 2026, LLMs..." |153154### Best practices from professionals155156| Practice | Source | Why |157|----------|--------|-----|158| Always set `printBackground: true` | BrowserStack, Playwright docs | Kami uses parchment bg #f5f4ed — Chromium strips it by default |159| Use `file://` protocol | Stack Overflow, DocuPotion | Avoids auth/CORS issues with local files |160| Set `waitUntil: 'networkidle'` | BrowserStack | Ensures fonts, CSS fully loaded |161| Lock browser version in CI | BrowserStack, blog.rasc.ch | Chromium updates can change rendering |162| Use `@page` margins, not Playwright margins | print-css.rocks, CSS Paged Media spec | CSS margins are more predictable for paged media |163| `-webkit-print-color-adjust: exact` | MDN, Chromium docs | Critical for parchment backgrounds and brand colors |164| Reuse browser instance (warm) | pdf4.dev benchmark | 42ms → 3ms speedup (14x) |165| Validate PDF visually in CI | BrowserStack | Page count, whitespace, font check |166167## Warm mode (production pipeline)168169```js170const { chromium } = require('playwright');171172class PDFRenderer {173 constructor() {174 this.browser = null;175 }176177 async start() {178 this.browser = await chromium.launch();179 }180181 async render(htmlPath, outputPath) {182 const page = await this.browser.newPage();183 await page.goto('file:///' + htmlPath.replace(/\\/g, '/'), {184 waitUntil: 'networkidle'185 });186 await page.pdf({187 path: outputPath,188 format: 'A4',189 printBackground: true,190 margin: { top: '0', bottom: '0', left: '0', right: '0' }191 });192 await page.close();193 }194195 async stop() {196 await this.browser.close();197 }198}199```200201## Font embedding202203Chromium embeds system fonts by default. For custom fonts (like TsangerJinKai02 in Kami):204205```css206@font-face {207 font-family: "CustomFont";208 src: url("fonts/CustomFont.woff2") format("woff2");209 font-weight: 400;210 font-style: normal;211}212```213214Place font files relative to the HTML and use relative `src` paths. Chromium resolves them from the HTML file's directory.215216## Troubleshooting217218| Problem | Fix |219|---------|-----|220| White background instead of parchment | Add `printColorAdjust: exact` in CSS and `printBackground: true` in JS |221| Fonts not rendering | Use relative paths in `@font-face`, check font file exists |222| Page breaks wrong | Check `break-inside: avoid` on tables, pre, callout |223| Near-empty page (2 lines alone) | Merge short section with previous; avoid `break-before: page` on light chapters |224| Content overflow | Use `page-break-inside: avoid` on large blocks |225| Chinese chars as boxes | Include CJK font in `@font-face` or use system CJK fallback |226| PDF too large | Remove unnecessary images, compress embedded fonts |227| Slow first render | Keep browser instance alive (warm mode) |228229### CSS visual pattern snippets230231Concrete patterns to include in the HTML by document type:232233```css234/* Severity badges (pentest, security) */235.badge-high { background: #f5e8e8; color: #a83030; display: inline-block; padding: 2pt 7pt; border-radius: 3pt; font-size: 8pt; font-weight: 500; text-transform: uppercase; }236.badge-medium { background: #f5ede2; color: #b86a25; display: inline-block; padding: 2pt 7pt; border-radius: 3pt; font-size: 8pt; font-weight: 500; text-transform: uppercase; }237.badge-ok { background: #e8f2ec; color: #2a7a4a; display: inline-block; padding: 2pt 7pt; border-radius: 3pt; font-size: 8pt; font-weight: 500; text-transform: uppercase; }238239/* Risk bar (pentest exec summary) */240.risk-bar { display: flex; gap: 2pt; margin: 8pt 0 14pt 0; height: 12pt; }241.risk-bar .seg { border-radius: 2pt; display: flex; align-items: center; justify-content: center; font-size: 7pt; color: #fff; font-weight: 500; }242243/* Code blocks with left border (pentest, technical) */244pre { border-left: 2.5pt solid #1B365D; border-radius: 3pt; background: #faf9f5; padding: 10pt 14pt; font-family: Consolas, monospace; font-size: 9pt; line-height: 1.5; break-inside: avoid; }245246/* Impact/Remediation boxes (pentest) */247.impact-box, .remediation-box { border-left: 2pt solid #e8e6dc; padding-left: 12pt; margin: 10pt 0 14pt 0; break-inside: avoid; }248.impact-box .label, .remediation-box .label { font-size: 8.5pt; letter-spacing: 0.8pt; text-transform: uppercase; font-weight: 500; color: #1B365D; margin-bottom: 4pt; }249250/* Cover accent line */251.cover-line { width: 60pt; height: 2pt; background: #1B365D; margin: 20pt 0; border-radius: 1pt; }252253/* Pipeline flow (4 step horizontal) */254.pipeline-flow { display: flex; gap: 0; margin: 18pt 0; break-inside: avoid; }255.pipeline-step { flex: 1; text-align: center; padding: 10pt 8pt; }256.pipeline-step .dot { width: 6pt; height: 6pt; border-radius: 50%; background: #1B365D; margin: 0 auto 5pt auto; }257.pipeline-step + .pipeline-step { border-left: 0.5pt dotted #e8e6dc; }258.pipeline-step .name { font-size: 9pt; font-weight: 500; color: #141413; margin-bottom: 3pt; }259.pipeline-step .desc { font-size: 7.5pt; color: #6b6a64; line-height: 1.35; }260261/* Keep-together wrapper */262.keep-together { break-inside: avoid; }263```264265### Self-review protocol (mandatory)266267Don't generate the PDF without passing this checklist. Each item is a real failure documented in previous iterations.268269### Structural (blocking)270- [ ] Each chapter has enough content to fill >1/3 of a page271- [ ] No sections with just heading + 1 short paragraph (merge)272- [ ] No near-empty pages (2 lines alone)273- [ ] `break-before: page` only on chapters with dense content274275### Visual (high priority)276- [ ] Visual elements match the document type (badges, risk bars, code blocks, etc.)277- [ ] Tables have proper styling (optional zebra, padding, clear headers)278- [ ] Code blocks are distinguishable from body (border, background, monospace)279- [ ] No repeated em dashes `—` used as label/value separators280- [ ] There's variety in visual rhythm (not all pages the same)281282### Content (high priority)283- [ ] Every categorical claim has its visible source ("according to X...")284- [ ] Internal methodology is distinguished from verified fact (disclaimer if applicable)285- [ ] No redundant content between chapters (sources, repeated lists)286- [ ] Every paragraph is necessary (if it can be removed without loss, remove it)287288### Technical (blocking)289- [ ] `printBackground: true` in Playwright configuration290- [ ] `-webkit-print-color-adjust: exact` in CSS291- [ ] `widows: 4; orphans: 4` in @page and body292- [ ] `break-inside: avoid` on pre, table, blockquote, callout293- [ ] `table tr { break-inside: avoid }` so tables don't split rows294295## Complete HTML example296297Minimum functional template that includes all essential patterns. Copy and adapt:298299```html300<!DOCTYPE html>301<html lang="en">302<head>303<meta charset="UTF-8">304<title>Document Title</title>305<style>306 @page { size: A4; margin: 24mm 26mm 26mm 26mm; background: #f5f4ed; widows: 4; orphans: 4; }307 * { box-sizing: border-box; margin: 0; padding: 0; -webkit-print-color-adjust: exact; }308 :root {309 --parchment: #f5f4ed; --ivory: #faf9f5; --near-black: #141413;310 --dark-warm: #3d3d3a; --olive: #504e49; --stone: #6b6a64;311 --brand: #1B365D; --border: #e8e6dc; --border-soft: #e5e3d8;312 --serif: Charter, Georgia, Palatino, "Times New Roman", serif;313 --mono: Consolas, "Courier New", monospace;314 }315 body { font-family: var(--serif); font-size: 10.5pt; line-height: 1.65; color: var(--near-black); background: var(--parchment); widows: 4; orphans: 4; }316 h1 { font-size: 24pt; border-left: 2.5pt solid var(--brand); padding-left: 8pt; margin: 0 0 10pt 0; break-after: avoid; }317 h2 { font-size: 16pt; margin: 24pt 0 8pt 0; break-after: avoid; }318 p { margin: 0 0 10pt 0; widows: 3; orphans: 3; }319 table { width: 100%; border-collapse: collapse; font-size: 9.5pt; margin: 12pt 0; break-inside: avoid; }320 table th { text-align: left; padding: 6pt 8pt; border-bottom: 1pt solid var(--border); background: var(--ivory); }321 table td { padding: 5pt 8pt; border-bottom: 0.3pt solid var(--border-soft); }322 table tr { break-inside: avoid; }323 pre { border-left: 2.5pt solid var(--brand); border-radius: 3pt; background: var(--ivory); padding: 10pt 14pt; font-size: 9pt; font-family: var(--mono); break-inside: avoid; margin: 6pt 0 10pt 0; }324 .callout { background: var(--ivory); border-left: 2pt solid var(--brand); padding: 10pt 14pt; border-radius: 3pt; margin: 12pt 0; break-inside: avoid; }325 .keep-together { break-inside: avoid; }326 .break { break-before: page; }327 /* Add visual patterns by document type (badges, risk bar, etc.) */328</style>329</head>330<body>331<!-- Cover -->332<section style="min-height:240mm;display:flex;flex-direction:column;justify-content:space-between;padding:40mm 0 0 0;break-after:page;">333 <div>334 <div style="font-size:10pt;color:var(--brand);letter-spacing:2pt;text-transform:uppercase;margin-bottom:18pt;">Document Type</div>335 <div style="font-size:40pt;font-weight:500;line-height:1.12;margin-bottom:16pt;">Main Title</div>336 <div style="width:60pt;height:2pt;background:var(--brand);margin:20pt 0;border-radius:1pt;"></div>337 <div style="font-size:14pt;color:var(--olive);max-width:85%;">Subtitle or description</div>338 </div>339 <div style="font-size:10pt;color:var(--stone);">Author · Date</div>340</section>341<!-- Chapter -->342<section class="break">343 <h1>Chapter Title</h1>344 <p>Document content. Verify sources (research), humanize text (humanize), design with Kami.</p>345 <!-- Tables, lists, callouts as needed -->346</section>347</body>348</html>349```350351## Integration with Kami352353```powershell354# One-liner from Kami HTML to PDF355npx playwright pdf "file:///path/to/kami-output.html" "final.pdf"356```357358## Sources359360- [Playwright PDF API docs](https://playwright.dev/docs/api/class-page#page-pdf)361- [pdf4.dev HTML-to-PDF benchmark 2026](https://pdf4.dev/blog/html-to-pdf-benchmark-2026)362- [BrowserStack: Generate PDFs with Playwright](https://www.browserstack.com/guide/playwright-pdf-html-generation)363- [print-css.rocks: CSS Paged Media tutorial](https://print-css.rocks/)364- [DocuPotion: Generate PDFs with Playwright](https://docupotion.com/blog/generate-pdfs-playwright)365- [CSS Paged Media W3C specification](https://www.w3.org/TR/css-page-3/)366- [Blog.rasc.ch: Generating PDFs with Playwright and Go](https://blog.rasc.ch/2026/03/playwrightpdf.html)367- [HN: What is the open-source way to convert HTML to PDF](https://news.ycombinator.com/item?id=45404760)368369## Workflow370371### Step 1: Validate HTML template372373Before rendering, verify the template is production-ready:3741. Open the HTML in a browser first: confirm fonts load, CSS applies, layout renders correctly at print size3752. Check that `@page` rules specify explicit size (`A4`, `Letter`) and margins3763. Verify `-webkit-print-color-adjust: exact` and `print-color-adjust: exact` are set on `*`3774. Confirm all `@font-face` declarations use relative paths, not absolute filesystem paths378379### Step 2: Configure render settings3803811. Set `format` (A4, Letter, A3) to match the template's `@page { size: }` declaration3822. Set `printBackground: true` — always. Never skip this. Chromium strips backgrounds by default3833. Set Playwright `margin: { top: '0', bottom: '0', left: '0', right: '0' }` — use CSS `@page` margins instead3844. Use `waitUntil: 'networkidle'` to ensure fonts, CSS, and images are fully loaded before rendering385386### Step 3: Render PDF3873881. Launch browser once (warm mode) — reuse for all documents in batch3892. Navigate with `file://` protocol to avoid CORS and authentication issues with local files3903. Await `page.pdf()` with configured options3914. Close page after each render but keep browser alive for next document392393### Step 4: Validate output PDF3943951. Open PDF and check: parchment/tinted background rendered (not white), fonts embedded correctly, page breaks occur at logical points3962. Scan every page for near-empty pages (2 lines or less floating at page top)3973. Verify code blocks, callouts, and tables are not split across page boundaries3984. Check CJK characters render correctly if document contains Chinese, Japanese, or Korean text399400### Step 5: Apply visual patterns by document type4014021. Pentest/security report: severity badges, risk bars, finding headers, styled code blocks, impact/remediation boxes4032. One-pager/executive summary: glance grid (4 metrics), lead paragraph, takeaway box, cover with decorative accent line4043. White paper/long document: chapter breaks on dense sections, callouts, keep-together wrappers on critical blocks4054. Letter: wide margins (25mm), formal greeting/closing, single-column, no unnecessary breaks4065. Resume: dense body (9.2pt), metric row, project bullets with action + result format407408### Step 6: Run self-review protocol409410Complete ALL four checklist categories before final output: Structural (blocking), Visual (high priority), Content (high priority), Technical (blocking). Every item in the self-review protocol is a real failure mode documented from previous iterations.411412## Error Handling413414| Cause | Fix |415|-------|-----|416| PDF renders with stark white background instead of parchment (#f5f4ed) | Chromium strips all backgrounds in print mode. Ensure BOTH `printBackground: true` in Playwright JS config AND `-webkit-print-color-adjust: exact` on `*` in CSS |417| Custom fonts render as fallback system serif in the PDF output | Use relative paths in `@font-face` `src: url("fonts/CustomFont.woff2")`. Place font files in same directory as HTML or a subdirectory. Chromium resolves relative to the HTML file's location |418| CJK characters (Chinese, Japanese, Korean) render as empty boxes or tofu | Include a CJK-capable font via `@font-face` or add system CJK fallback stack: `font-family: "TsangerJinKai02", "SimSun", "MS Mincho", serif`. Test before batch rendering |419| Near-empty page at end of section (2 lines of text floating alone) | Merge the short section with the previous one. Only apply `break-before: page` to chapters with > 1/3 page of content. Count elements: if heading + paragraph count < 5 total, don't force a page break |420| Table rows split awkwardly across consecutive pages | Add `table tr { break-inside: avoid; }` in CSS. For large spanning tables, add `table { break-inside: avoid; }` so the entire table moves as a block |421| Page break leaves an isolated heading at the very bottom of a page | Add `h1, h2, h3, h4 { break-after: avoid; }` to ensure at least the first content element after a heading stays with it on the same page |422| PDF file size is excessive (10MB+ for a text-heavy document) | Remove unnecessary raster images. Subset and compress embedded fonts to only used glyphs. Reduce DPI of any embedded raster content. Check for duplicated embedded resources |423| `page.pdf()` call hangs indefinitely on `networkidle` with dynamic/JS-rendered content | Switch to `waitUntil: 'load'` or add explicit `page.waitForTimeout(3000)`. For charts and JS-rendered content, use `waitForSelector()` on a known rendered element |424| Chromium version mismatch between local dev and CI causes visual output differences | Lock Chromium version explicitly: note the version `npx playwright install chromium` installs. Document it in CI config. Re-baseline visual checks on every Chromium version bump |425| Multiple documents rendered in bulk — browser crashes after N documents due to memory | Implement page pooling: close and reopen pages every 10-20 documents. Or restart browser every 50 documents. Memory leak in Chromium PDF rendering is a known issue |426427## Anti-Patterns428429| Pattern | Problem | Fix |430|---------|---------|-----|431| Setting margins via Playwright `margin` parameter instead of CSS `@page` | Playwright margin rendering is inconsistent across Chromium versions. CSS `@page` margins produce predictable, standards-compliant output | Always use `@page { margin: 24mm 26mm 26mm 26mm; }` in CSS. Set Playwright `margin: { top: '0', bottom: '0', left: '0', right: '0' }` |432| `break-before: page` applied to every section/chapter unconditionally | Creates near-empty pages when a section has little content. Reader sees a page with 2 lines. Looks unprofessional and amateur | Only force page breaks on chapters with > 1/3 page of content. Count paragraph + table + list elements. If < 5 total content elements, skip the forced break |433| All pages share identical visual structure (title → table → title → table) | Monotonous reading experience. Reader disengages after page 3. Document fails to sustain attention through key findings | Vary rhythm: finding box, then risk bar, then callout, then table, then narrative paragraph. Alternate between data-dense and commentary pages |434| Repeated em dashes used as label/value separator in tables and lists | "Black Box — no credentials" repeated 15 times in a pentest report. Jarring, lazy, reads like raw data dump | Use colon separator (`Black Box: no credentials`), parentheses, or proper table columns with distinct header and value styling |435| Code blocks visually indistinguishable from surrounding body text | Reader skims past code assuming it's prose. Reduces document credibility. Technical content loses all impact | Style every code block: 2.5pt left border, ivory/tinted background, monospace font (Consolas/Courier), generous padding (10-14pt), slight border-radius |436| `printBackground: false` anywhere in the production rendering pipeline | Chrome strips all background colors, including parchment. Document renders on stark white default. Destroys the Kami visual design | Always `printBackground: true`. Add CI assertion: if PDF renders with white background, fail the build |437| Launching a new browser instance per PDF document | Cold start ~630ms vs warm ~13ms. 48x slower per document. Wastes CI minutes on multi-document projects | Use warm mode (PDFRenderer class pattern): launch browser once, render all documents, close browser once. Keep-alive between renders |438| Font files referenced with absolute Windows paths (`C:\Users\swagger\fonts\...`) | PDF generated on a different machine or in CI won't find fonts. Paths break across environments. Not portable | Always use relative paths in `@font-face`: `src: url("fonts/CustomFont.woff2")`. Place font files in same directory or subdirectory as the HTML template |439| Skipping the self-review protocol because "it looks fine in the browser" | Browser rendering != PDF output. Chrome applies different print stylesheet rules. Many issues only visible in the final PDF | Always open the actual PDF output and run through all 4 checklist categories. Visual inspection is mandatory, not optional |440441## Checklist442443- [ ] Skill loads without errors in the AI agent444- [ ] YAML frontmatter is valid (description, compatibility, audience)445- [ ] Workflow section provides clear step-by-step instructions446- [ ] Error handling section covers common failure modes447- [ ] All referenced files (references/, scripts/, assets/) exist448- [ ] Skill triggers correctly for intended use cases449- [ ] No broken links or missing resources