Files
Adnan Zahir 98dca237f4 Add infra-ops-toolkit plugin: topology diagram, html-to-pdf, teleport onboarding skills
Distills patterns from the Teleport access topology work: verified-data diagram
building, as-displayed HTML-to-PDF export via headless Chromium, and
container-scoped/host/database onboarding into an existing Teleport cluster.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3ZTfgQrkR3q8DSEoZAmvs
2026-09-08 16:11:36 +07:00

4.0 KiB

name, description
name description
html-to-pdf 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

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
// 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); });
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.