Deck → interactive HTML
Turn a .pptx / .pdf into one self-contained .html file that presents better than
the original: charts redrawn natively so they animate, screenshots kept where the
screenshot is the evidence, live demos embedded in-slide, and a clean console.
Everything here was learned building a real investor deck. Follow the order.
Non-negotiables
- Never fabricate data. Recover real numbers (
references/04-chart-data-recovery.md) or keep the source image. A plausible-looking invented curve in an investor deck is a serious failure, not a shortcut. - Content is verbatim unless the user says otherwise. Fix typos and awkward phrasing; flag anything factually inconsistent rather than silently "improving" it.
- One HTML file. All CSS, JS, fonts and images inlined. It must work with the network off — you cannot debug a missing CDN from a stage.
- QA is not optional. Every slide screenshotted, 0 console errors, 0 overflow.
references/06-qa-and-export.md. - Load the
slide-designskill too if it is available — it governs what goes on a slide. This skill governs how it is built.
Workflow
1 — Read the source before building anything
markitdown deck.pptx > dump.md # text, per slide
soffice --headless --convert-to pdf deck.pptx # LibreOffice; in Claude's sandbox:
# python scripts/office/soffice.py ...
pdftoppm -jpeg -r 100 deck.pdf slide # visual reference for every page
python3 -c "import zipfile;zipfile.ZipFile('deck.pptx').extractall('unpacked')"
Look at every rendered page. Then classify each slide:
| Source slide is… | Rebuild as |
|---|---|
| Text, tables, bullet lists | Native HTML |
| A chart drawn with PPT shapes | Native SVG — recover the data from the shapes |
| A chart pasted as an image | Native SVG — recover the data from pixels |
| A screenshot that is the evidence (leaderboard, news card, product UI) | Keep the image + animated SVG overlay |
| A URL / demo | Native slide + live <iframe> demo overlay |
Full extraction recipes, hidden-slide detection and image↔slide mapping:
references/01-extract-source.md.
2 — Build the generator, not the HTML
Never hand-write a 5000-line HTML file. Write a Python generator:
src/build.py slide definitions -> emits the single .html
src/style.css design system + components
src/app.js runtime (nav, demos, tooltips, count-up)
src/charts.py native SVG chart builders
src/assets.py image -> base64 WebP data URI
build.py inlines style.css and app.js and replaces /*__DEMOS__*/ and
/*__TITLES__*/ placeholders. Copy assets/ from this skill as the starting point —
they are the proven versions, not sketches. references/02-architecture.md.
3 — Commit to one design system
Fixed 1280×720 stage, transform: scale() to fit any viewport. One background tone
for the whole deck. Every content slide is:
kicker · optional numbered badge + h1 · one-line sub · .body
Same paddings, same type scale, one accent colour. If the deck has a numbered agenda slide ("Five Advantages"), the badges on the detail slides must match that list — and must stay in deck order. Reordering slides means renumbering both.
4 — Animate as sequencing, not decoration
[data-anim] + inline transition-delay, gated on .is-active. The entrance order is
the order you speak in. Slide-to-slide transitions stay quiet — a fast fade and a 1%
nudge, never 3D. references/03-animation.md.
5 — QA, every time
node qa.js # per-slide overflow + pageerrors (ACTIVE slide only — see gotchas)
node console.js # full console + requestfailed sweep
node shot.js # screenshot every slide, then LOOK at them
6 — Export
node topdf.js && python3 make_pdf.py → 16:9 PDF with animations settled.
references/06-qa-and-export.md.
The gotchas that will cost you hours
Read references/07-gotchas.md before writing CSS. The five that bite hardest:
- Two
styleattributes on one element — the second is silently dropped, so yourtransition-delayvanishes and sequencing breaks. Merge them. preserveAspectRatio="none"stretches text. Measure the rendered container and match the viewBox aspect, or usemeet.flex:1inside a non-flex parent collapses to zero height — the chart renders blank.- Overflow checks give false positives on inactive slides — entrance transforms inflate
scrollHeight. Only measure.slide.is-active. - Percentage
max-heightdoes not resolve against an auto-height parent. Constrain by width and let height follow.
Reference index
| File | Covers |
|---|---|
references/01-extract-source.md |
pptx/pdf extraction, hidden slides, media mapping, PPT-shape chart recovery |
references/02-architecture.md |
generator layout, stage/scaling, slide skeleton, asset pipeline |
references/03-animation.md |
[data-anim], SVG draw/grow, offset-path, count-up, sequencing rules |
references/04-chart-data-recovery.md |
pixel-extracting real data from a chart image, with calibration + validation |
references/05-image-overlays.md |
registering an animated SVG onto a screenshot |
references/06-qa-and-export.md |
Playwright QA scripts, console hygiene, PDF export |
references/07-gotchas.md |
every trap hit in production, with the fix |
assets/ ships working style.css, app.js, assets.py, and all QA/export scripts.