--- name: html-to-pdf description: Export a local HTML file to a PDF that looks exactly like the live browser rendering — no shrink-to-fit distortion, no forced page splits, no vanished sticky/grid elements. Use when the user says a browser's own Print-to-PDF/Print dialog produced a broken, tiny, or wrongly-paginated result and they want the PDF "as-is / seperti yang tampil di browser" instead of a print-reflowed version. --- # HTML → PDF, exactly as displayed ## Why the browser's own Print dialog fails for this A system print dialog always targets a **fixed paper size** (Letter/A4). For any page wider than ~800px of real content — a multi-column CSS Grid layout, a wide diagram — the browser either shrinks the *entire* page to fit one sheet (readable content becomes a postage stamp) or splits it across pages in ways that don't match what's on screen. Two extra failure modes compound this: - `position: sticky` elements often just vanish or mis-render under the print media type. - If the page has its own `@media print` rules (e.g. for a *deliberately* paginated print layout), those override the normal on-screen styling — which is the opposite of what "as-is" means here. Neither "no print CSS at all" nor "add print CSS to force page-breaks" gives you the *live browser view* as a PDF. The actual fix is to render with a **page size equal to the content's own dimensions**, which no manual print dialog lets you set. ## The fix: headless Chromium in screen mode, content-sized page ```bash mkdir -p /tmp/pdfgen && cd /tmp/pdfgen npm init -y >/dev/null 2>&1 npm install puppeteer # bundles a compatible Chromium — no separate browser install needed ``` ```js // render.js const puppeteer = require('puppeteer'); const path = require('path'); (async () => { const srcPath = '/absolute/path/to/source.html'; const outPath = '/absolute/path/to/output.pdf'; const browser = await puppeteer.launch(); const page = await browser.newPage(); // Match the viewport width the page's CSS was actually designed around // (check its max-width / .page container, not an arbitrary guess). await page.setViewport({ width: 1940, height: 1200, deviceScaleFactor: 2 }); await page.goto('file://' + srcPath, { waitUntil: 'networkidle0' }); // The critical line: force normal on-screen styles, ignoring any // @media print rules the page might define for its own purposes. await page.emulateMediaType('screen'); // Measure the real rendered size so the PDF page is exactly that size — // one continuous page, nothing scaled, nothing split. const { width, height } = await page.evaluate(() => { const el = document.querySelector('.page') || document.body; const rect = el.getBoundingClientRect(); return { width: Math.ceil(rect.width), height: Math.ceil(document.documentElement.scrollHeight) }; }); await page.pdf({ path: outPath, width: `${width}px`, height: `${height}px`, printBackground: true, // otherwise CSS background colors silently disappear margin: { top: 0, right: 0, bottom: 0, left: 0 }, }); await browser.close(); console.log('Saved:', outPath, width, 'x', height); })().catch(err => { console.error(err); process.exit(1); }); ``` ```bash node render.js ``` ## Notes - `printBackground: true` is easy to forget and its absence is easy to misdiagnose as "the PDF looks washed out" rather than "backgrounds are just missing." - If the page measures its own scroll height *before* web fonts finish loading, dimensions can be slightly off — `waitUntil: 'networkidle0'` on `goto` covers most cases; add an explicit `page.evaluateHandle('document.fonts.ready')` await first if fonts still look unloaded in the output. - This same technique works for any "I want the PDF to look like the page, not like a printout" request — it's not specific to any one diagram or document. - Verify the result before calling it done: read the generated PDF back (most tools can render/preview a PDF's first page) and compare it against the live browser view side by side, don't just trust that `page.pdf()` succeeded without error.