PDF Generation with Claude Code: Build Invoices and Reports from HTML
Implement PDF generation with Claude Code, Playwright, print CSS, fonts, invoices, reports, and visual checks.
Adding a “Download PDF” button sounds small until the first real business document breaks. Invoices need exact totals, tax lines, page margins, readable tables, and stable Japanese or multilingual text. Monthly reports need headings, charts, notes, and page breaks that do not split a section in a strange place. Certificates need names, dates, IDs, logos, and a layout that still looks trustworthy when printed.
The practical path is not to draw every line by hand. For most web teams, the most maintainable approach is HTML-to-PDF with a real browser renderer. Build the document as HTML, write print CSS, let Chromium render it, then export the page as a PDF with Playwright or Puppeteer. Claude Code can implement this quickly, but only if the prompt asks for print-ready HTML, font handling, page size, visual checks, and failure cases instead of a fragile canvas-only demo.
Use official sources as the baseline. Playwright documents page.pdf and media emulation. Puppeteer documents PDF generation and PDFOptions. MDN covers @page and printing CSS. For Claude Code workflow, start from the Claude Code overview. For adjacent ClaudeCodeLab guides, pair this with spreadsheet automation, Playwright testing, and testing strategies.
Choose HTML-to-PDF first
There are three common PDF strategies. A low-level PDF library such as jsPDF draws text and lines at coordinates. It is useful for tiny labels or simple one-page forms, but complicated invoices become a pile of x and y positions. A screenshot or canvas approach renders the document as an image and places that image inside a PDF. It can look right in a quick review, but the text is hard to search, copy, translate, or audit. The third option is to render HTML with CSS and ask Chromium to print it.
HTML-to-PDF is usually the best starting point because it uses skills web developers already have. Tables remain tables. Headings remain headings. Print margins are handled with @page. The same document can be opened in a browser for review, screenshot testing, and debugging before it is exported. That makes Claude Code more useful because it can edit normal HTML, CSS, and test files instead of calculating document coordinates.
The tradeoff is that print CSS is its own medium. A screen layout with sticky headers, heavy shadows, animated elements, and viewport-based sizing often fails when printed. Treat the PDF view as a dedicated print surface. It can share data with your app, but it should have its own template and CSS rules.
flowchart TD
A["Business data"] --> B["HTML template"]
B --> C["Print CSS and @page"]
C --> D["Chromium rendering"]
D --> E["PDF file"]
C --> F["Screenshot comparison"]
F --> G["Review evidence"]
Real use cases
The first use case is an invoice. It needs seller details, buyer details, line items, tax, total, due date, bank note, and a stable invoice number. The dangerous bugs are not decorative. They are incorrect rounding, missing tax, clipped totals, or a font fallback that changes row height and pushes the footer onto a new page.
The second use case is a monthly report. A report may combine a summary, tables, screenshots, charts, and recommendations. The key problem is page flow. A heading should not be left alone at the bottom of a page, and a chart should not be separated from its explanation. break-inside: avoid helps for small blocks, but long tables still need deliberate layout decisions.
The third use case is a certificate or completion document. It usually fits on one page and includes a name, course, date, certificate ID, logo, and sometimes a QR code. A canvas-only certificate may look fine, but searchable text and smaller file size matter when many documents are stored or emailed.
The fourth use case is an audit or compliance report. Deployment logs, approval records, permissions, and incident notes often become PDFs for non-engineering reviewers. Add the generated date, environment, application version, and source data ID so the PDF can be traced later.
Prompt Claude Code with print constraints
A strong prompt tells Claude Code which rendering path to use, what files to touch, and what verification evidence to leave. It also says what not to do.
Implement PDF generation with Claude Code.
Direction:
- Render an HTML template with Playwright Chromium and export it to PDF.
- Do not render the whole document as one canvas image.
- Use A4 portrait, 14mm margins, visible backgrounds, and print CSS.
- Use font fallbacks that work for Japanese and Latin text.
- Keep the structure reusable for invoices, reports, and certificates.
Implementation:
- Add scripts/create-invoice-pdf.mjs.
- Generate out/invoice-T-2026-0602.pdf from sample data.
- Format money with Intl.NumberFormat.
- Escape user-provided text before inserting it into HTML.
- Use printBackground and preferCSSPageSize in page.pdf.
Verification:
- Document the run command.
- Make the print HTML suitable for screenshot comparison.
- Check fonts, page breaks, backgrounds, and totals.
This prevents common weak outputs: pseudocode, coordinate-only jsPDF, a screenshot pretending to be a document, or an API route with no visual verification.
Copy-paste runnable Playwright script
Create a small Node project and install Playwright:
npm init -y
npm pkg set type=module
npm i -D playwright
npx playwright install chromium
mkdir scripts out
node scripts/create-invoice-pdf.mjs
Create scripts/create-invoice-pdf.mjs:
import { chromium } from "playwright";
import { mkdir } from "node:fs/promises";
import { dirname, resolve } from "node:path";
const outputPath = resolve("out/invoice-T-2026-0602.pdf");
const invoice = {
number: "T-2026-0602",
issuedAt: "2026-06-02",
dueAt: "2026-06-30",
seller: "Masa Design Lab",
buyer: "Sample Co., Ltd.",
note: "Invoice for Claude Code enablement and PDF template implementation.",
items: [
{ name: "PDF template design", quantity: 1, unitPrice: 800 },
{ name: "Playwright generation script", quantity: 1, unitPrice: 1200 },
{ name: "Print CSS and font verification", quantity: 1, unitPrice: 600 },
],
taxRate: 0.1,
};
const money = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});
function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, (char) => ({
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
})[char]);
}
function renderInvoiceHtml(data) {
const subtotal = data.items.reduce(
(sum, item) => sum + item.quantity * item.unitPrice,
0,
);
const tax = Math.round(subtotal * data.taxRate);
const total = subtotal + tax;
const rows = data.items.map((item) => {
const amount = item.quantity * item.unitPrice;
return `<tr>
<td>${escapeHtml(item.name)}</td>
<td class="num">${item.quantity}</td>
<td class="num">${money.format(item.unitPrice)}</td>
<td class="num">${money.format(amount)}</td>
</tr>`;
}).join("");
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Invoice ${escapeHtml(data.number)}</title>
<style>
@page { size: A4; margin: 14mm; }
* { box-sizing: border-box; }
body {
margin: 0;
color: #202124;
font-family: "Noto Sans JP", "Yu Gothic", Arial, sans-serif;
font-size: 12px;
line-height: 1.7;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.sheet {
min-height: 269mm;
display: flex;
flex-direction: column;
gap: 18px;
}
header {
display: flex;
justify-content: space-between;
gap: 24px;
border-bottom: 3px solid #1f5eff;
padding-bottom: 16px;
}
h1 { margin: 0 0 10px; font-size: 28px; letter-spacing: 0; }
.meta { text-align: right; color: #4b5563; }
.parties {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.box {
border: 1px solid #d7dce5;
border-radius: 6px;
padding: 14px;
background: #f8fafc;
}
table { width: 100%; border-collapse: collapse; margin-top: 8px; }
th { background: #eef3ff; color: #1f2937; text-align: left; }
th, td { border-bottom: 1px solid #d7dce5; padding: 10px 8px; }
.num { text-align: right; white-space: nowrap; }
.totals { margin-left: auto; width: 260px; }
.totals div {
display: flex;
justify-content: space-between;
padding: 6px 0;
}
.grand-total {
margin-top: 4px;
border-top: 2px solid #1f5eff;
font-size: 18px;
font-weight: 700;
}
.avoid-break { break-inside: avoid; page-break-inside: avoid; }
footer {
margin-top: auto;
border-top: 1px solid #d7dce5;
padding-top: 10px;
color: #6b7280;
font-size: 10px;
}
</style>
</head>
<body>
<main class="sheet">
<header>
<div>
<h1>Invoice</h1>
<div>${escapeHtml(data.buyer)}</div>
</div>
<div class="meta">
<div>Invoice: ${escapeHtml(data.number)}</div>
<div>Issued: ${escapeHtml(data.issuedAt)}</div>
<div>Due: ${escapeHtml(data.dueAt)}</div>
</div>
</header>
<section class="parties avoid-break">
<div class="box"><strong>Seller</strong><br>${escapeHtml(data.seller)}</div>
<div class="box"><strong>Note</strong><br>${escapeHtml(data.note)}</div>
</section>
<section>
<table>
<thead>
<tr>
<th scope="col">Item</th>
<th scope="col" class="num">Qty</th>
<th scope="col" class="num">Unit</th>
<th scope="col" class="num">Amount</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</section>
<section class="totals avoid-break">
<div><span>Subtotal</span><span>${money.format(subtotal)}</span></div>
<div><span>Tax</span><span>${money.format(tax)}</span></div>
<div class="grand-total"><span>Total</span><span>${money.format(total)}</span></div>
</section>
<footer>Generated by Playwright. Verify layout, fonts, totals, and page breaks before sending.</footer>
</main>
</body>
</html>`;
}
async function createPdf() {
await mkdir(dirname(outputPath), { recursive: true });
const browser = await chromium.launch();
try {
const page = await browser.newPage();
await page.setContent(renderInvoiceHtml(invoice), { waitUntil: "networkidle" });
await page.evaluate(() => document.fonts.ready);
await page.emulateMedia({ media: "print" });
await page.pdf({
path: outputPath,
printBackground: true,
preferCSSPageSize: true,
tagged: true,
margin: { top: "0", right: "0", bottom: "0", left: "0" },
});
console.log(`Created ${outputPath}`);
} finally {
await browser.close();
}
}
await createPdf();
The key details are @page, printBackground, preferCSSPageSize, font readiness, and escaping text. The script uses CSS for paper size and passes zero margins to page.pdf so the PDF engine does not add a second margin layer.
Pitfalls to avoid
Do not turn the entire document into one image unless the document is truly visual art. Image-only PDFs are harder to search, bigger, blurry when zoomed, and difficult to regression test. Use images for logos, signatures, QR codes, and charts, but keep meaningful text as HTML.
Do not assume local fonts exist in production. A PDF that looks fine on a Windows laptop can reflow in Linux CI when Yu Gothic is missing. Install Noto CJK fonts in your container, load a licensed font, or keep a known font stack. Always wait for document.fonts.ready before exporting.
Do not mix screen spacing and paper spacing. A web page can rely on viewport width; an A4 page cannot. Put paper size and margins in @page, avoid fixed viewport heights, and test long text. If a block is taller than a page, break-inside: avoid cannot save it.
Do not skip HTML escaping. Invoices and reports often include customer names, notes, and project titles. Insert those values through an escaping function or a template engine with escaping enabled.
Visual regression and screenshots
PDF byte checks are useful, but they do not prove the layout is right. Test the print HTML before generating the PDF:
import { expect, test } from "@playwright/test";
test("invoice print layout is stable", async ({ page }) => {
await page.goto("/invoices/T-2026-0602/print");
await page.emulateMedia({ media: "print" });
await expect(page.getByRole("heading", { name: "Invoice" })).toBeVisible();
await expect(page.getByText("PDF template design")).toBeVisible();
await expect(page.getByText("$2,860.00")).toBeVisible();
await expect(page).toHaveScreenshot("invoice-print-a4.png", {
fullPage: true,
maxDiffPixelRatio: 0.01,
});
});
This test checks the print view, not every byte of the PDF. In production, add tests for totals, generated file existence, long line items, missing logos, and multi-page output.
Monetization CTA and verification
PDF generation is a strong monetization topic because it connects implementation with business outcomes: invoices, reports, certificates, lead magnets, and client deliverables. A useful article should give working code, failure cases, and verification evidence, then offer a natural next step. Start with the free Claude Code cheatsheet, use ClaudeCodeLab products for reusable prompts and templates, and use training or consultation when your team needs PDF generation, review rules, and Playwright checks built into a real repository.
For this article, I verified the workflow by using a local Node.js setup with Playwright Chromium, generating an invoice PDF, and checking the print-oriented HTML before export. The most valuable habit was not the PDF call itself; it was treating the print HTML as a testable surface. In Masa’s practical work, font loading, missing backgrounds, and page-break drift are more common than syntax errors, so Claude Code should be asked for both runnable implementation and verification evidence.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Obsidian to CLAUDE.md Workflow: Stop Re-explaining Context
Turn Obsidian working notes into concise CLAUDE.md operating notes that make Claude Code sessions easier to resume.
Claude Code Revenue CTA Routing: Send Articles to PDF, Gumroad, and Consultation
A Claude Code workflow for routing article readers to the free PDF, Gumroad products, or consultation by intent.
Claude Code Team Handoff Rules: Review Evidence, Permissions, Rollback, and Revenue Paths
A practical Claude Code handoff format for team review, proof, permission rules, rollback, free PDF, Gumroad, and consultation paths.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.