HTML Slide Deck to PDF Workflow
Build polished, presentation-ready HTML slide decks and convert them to razor-sharp PDFs using headless Chrome.
When to use
- Creating a new slide deck from program documentation, notes, or outlines.
- Converting rough content into a professional presentation.
- Generating a PDF version of an HTML deck with exact page dimensions.
- Building decks that include app screenshots or product visuals.
Progress checklist
Deck Build Progress:
- [ ] Phase 1: Slide skeleton (HTML + CSS foundation)
- [ ] Phase 2: Content population (slides from source material)
- [ ] Phase 3: Visual polish (spacing, typography, layout fixes)
- [ ] Phase 4: Print-safe CSS
- [ ] Phase 5: PDF conversion (use bundled script; do not reimplement)
- [ ] Phase 6: Verification (screenshot comparison)Phase 1 — Slide skeleton
Use the Domo theme — MANDATORY FIRST STEP
- Read references/domo-theme.md — it contains a complete, copy-paste-ready HTML skeleton that produces slides matching the Domo template exactly.
- Read references/assets/logo-data-uri.txt — it contains the actual Domo logo as a base64 PNG data URI. Replace every
LOGO_DATA_URIplaceholder in the skeleton with this value. - The logo PNG is also at references/assets/domologo.png.
Do not skip this step. Copy the complete skeleton from the theme file, then build slides into it. Every content slide MUST have:
- Header:
<div class="slide-header">with<h1>,.subtitle, and.header-line(thin gray separator — required) - Content:
<div class="slide-content has-bg">— always usehas-bgfor the gray#DDE5EDbackground (this is the Domo look; content is vertically centered automatically) - Footer:
<div class="slide-footer">with.confidential(left),.footer-line(center), and.domo-badgewith<img src="LOGO_DATA_URI">(bottom-right — required)
Dimensions
Standard 16:9 widescreen: --slide-w: 1024px; --slide-h: 576px;
Content area positioning
The has-bg class handles positioning automatically (position:absolute; top:90px; bottom:52px) with vertical centering (display:flex; justify-content:center). Do not override these with inline styles.
Phase 2 — Content population
Slide type patterns
Table slide — Use a styled <table> with labeled column headers. Keep cell padding tight (6px 10px) on dense slides.
Flow/pipeline slide — Horizontal flex row of boxes with arrow separators (▶). Use colored left borders or top borders to differentiate phases.
Card grid — Flex row of equal-width cards. Use flex:1 on each card, consistent padding, and subtle background colors (#F6F8FA).
Architecture diagram — Nested flex containers. Label each layer (e.g. "INTELLIGENCE LAYER", "STORAGE", "COMPUTE") with colored card groups.
Screenshot showcase — One hero image centered, or two side-by-side with max-width:48%. Apply border-radius:8px for polish. No box-shadow (removed by print CSS anyway).
Info boxes — Paired boxes below main content. Use display:flex; gap:32px with flex:1 on each.
Typography scale
| Element | Size | Weight |
|---|---|---|
| Slide title (h1) | 28px | 800 |
| Subtitle | 11px | 700 |
| Section label | 10px | 700, uppercase, letter-spacing 2px |
| Body text | 13–14px | 400 |
| Table text | 11–13px | 400 |
| Fine print / captions | 9–10px | 400 |
Color conventions
Use CSS variables for consistency. Define brand colors, accent colors, and text shades in :root.
Phase 3 — Visual polish
Spacing rules
- Content must never touch the footer. The
bottom:52pxon.slide-contentcreates a 52px exclusion zone. If content still overlaps, reducemargin-top,padding,gap,font-size, orline-heighton the offending elements. - Header–content gap. The
topvalue on.slide-contentcontrols clearance below the header. Typical:88–100px. Increase if subtitle text collides with content. - Dense slides. When a slide has both a table and info boxes, use compact values:
- Table cell padding: 4px 10px - Info box padding: 5px 12px - Gaps between cards: 3–5px - Font size: step down 1px from defaults
justify-content:centercaution. Vertical centering can push tall content pastbottom. Remove it on content-heavy slides and let content flow from top.
Image handling
- URL-encode spaces in
srcpaths:Sample%20Assets/file.png. - Avoid special Unicode characters in filenames. Copy to clean filenames if needed.
- Use
max-width:100%; max-height:100%withobject-fit:containto auto-size within the content area.
Phase 4 — Print-safe CSS
PDF rendering through Chromium's print pipeline treats box-shadow differently than screen rendering. Shadows appear as heavy dark blocks in PDF output.
Required rule (already in the skeleton above):
@media print {
*, *::before, *::after {
box-shadow: none !important;
}
}This strips all shadows for print only. The HTML browser view retains its shadows.
Additional print considerations:
page-break-after: alwayson.slide— one slide per page.page-break-inside: avoid— prevent mid-slide splits.background: whiteon body — no gray viewer background.
Phase 5 — PDF converter
Do not rewrite the converter from scratch. This skill ships a maintained script: references/convert-to-pdf.js. Copy it into the user’s project or run it by path from the skill directory after install.
npm install puppeteer-core
node convert-to-pdf.js path/to/deck.html path/to/deck.pdf
# optional second arg omitted => writes next to the HTML with .pdf extensionThe script uses puppeteer-core with the system Chrome binary (not bundled Chromium), matching the approach below.
Key settings (implemented in the script; change only if you have a deliberate reason):
deviceScaleFactor: 2— retina-crisp text.printBackground: true— preserves colored backgrounds.margin: {top: 0,...}— no extra whitespace around slides.width/heightmust match--slide-w/--slide-hexactly.- Wait for
document.fonts.readybefore capturing — Google Fonts must finish loading.
package.json setup
{
"scripts": { "pdf": "node convert-to-pdf.js" },
"dependencies": { "puppeteer-core": "latest" }
}Use puppeteer-core (not puppeteer): the full puppeteer package downloads its own Chromium on install (~hundreds of MB) and uses that browser; puppeteer-core is the automation library only and expects you to pass executablePath to an existing Chrome/Chromium (this script finds system Chrome). Smaller install, one browser to update, fewer CI/cache issues.
Phase 6 — Verification
Screenshot each HTML slide and each PDF page, then compare visually.
Quick verification script
// Screenshot slide N from the HTML
const box = await page.evaluate((idx) => {
const el = document.querySelectorAll(".slide")[idx];
const r = el.getBoundingClientRect();
return { x: r.x, y: r.y, width: r.width, height: r.height };
}, slideIndex);
await page.screenshot({ path: `html_slide_${n}.png`, clip: box });What to check
- Page count matches slide count.
- No content clipped by footer on any slide.
- Fonts loaded (not fallback serif/sans-serif).
- All images render (no broken-image icons or alt text).
- No dark hue/shadow blocks around cards or images.
- Background colors and gradients preserved.
- Page edges are razor-sharp (no extra margin or whitespace).
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| Content overlaps footer | bottom not set or justify-content:center on tall content | Set bottom:52px on .slide-content; remove vertical centering on dense slides |
| Dark blocks around elements in PDF | box-shadow renders differently in print | Add @media print {* {box-shadow: none!important;}} |
| Images don't load | Spaces or Unicode in file paths | URL-encode spaces (%20); copy files to clean filenames |
| Fonts fallback to serif | networkidle0 not waited or fonts blocked | Wait for document.fonts.ready; ensure Google Fonts URL is correct |
| Chrome not found | Puppeteer can't locate browser | Use puppeteer-core + executablePath to system Chrome |
| Extra whitespace around pages | PDF margin not zeroed | Set margin: {top: 0, right: 0, bottom: 0, left: 0} |
| Blurry text in PDF | Low device scale factor | Set deviceScaleFactor: 2 in viewport |
Checklist
- Slide dimensions match
--slide-wand--slide-hCSS variables - Every
.slide-contenthasbottom:52pxto clear the footer @media printblock strips allbox-shadowvaluespage-break-after: alwaysset on.slidefor print- Converter uses
puppeteer-corewith system Chrome path printBackground: trueand zero margins in PDF optionsdeviceScaleFactor: 2for retina quality- Font loading awaited before PDF capture
- Image paths URL-encoded (no raw spaces)
- Visual verification done on cover, dense slides, and screenshot slides