- The shape
- 1. Start from a library, not a blank page
- 2. The model writes a plan, a script writes the markup
- 3. No library? Write the slides, but only from the design system
- 4. Spend the best model on the argument only
- 5. HTML is the source, PowerPoint is derived
- 6. Check by measurement, then by eye
- 7. 1 file name for life
- How to build your own
- Gotchas
- Further reading
My client decks aren't written by an AI. They're assembled. A model picks slides from a library I already trust, writes the few lines that are about this client, and a script does everything else. Internal decks, which have no library to draw from, go through the same pipeline with a different guardrail: the design system.
That 1 decision fixed the problems I had with AI-generated slides: drifting markup, off-brand colours, text spilling out of boxes, and a 40-minute wait while the model re-typed HTML it had typed 100 times before. A client deck now takes minutes, and it looks the same every time.
This note explains the logic so anyone can build the same thing. It leaves out my employer's content and my own scripts. The pieces are open source: reveal.js for the HTML, python-pptx for PowerPoint, and Claude Code to run it. The earlier version of the system is in see NicDeck, and the survey that led to it is in see HTML Presentations for Agentic Workflows.
The shape
brief ─▶ plan.json ─▶ assemble ─▶ deck.html ─▶ QA ─▶ deliver
(model) (script) (source) (script + vision)
│
└─▶ deck.pptx (on request, composed from the HTML)
7 principles carry it.
1. Start from a library, not a blank page
Everything starts from a master deck: 1 HTML file with about 400 slides, grouped into chapters, each slide reviewed for facts, wording and design. Each slide has a stable id, a template name and a headline.
The library does 3 jobs:
- It holds the truth. Product facts, proof points and approved wording are fixed once, in 1 place, and every deck inherits the fix.
- It makes search cheap. A script turns the master into a map: 1 line per slide with its id, template and headline. A model ranks that map against the topic in seconds.
- It marks what must never ship. Internal slides (how to sell, objection answers) carry a flag, and the build refuses them in a client deck.
You don't need 400 slides to start. 30 good ones and a map beat any prompt.
2. The model writes a plan, a script writes the markup
The model never writes HTML. It writes a small JSON plan: which slides, in what order, what changes on each, and the 3 or 4 slides written for this audience. A generic example:
{
"deck": {"slug": "acme-onboarding", "client": "Acme", "lang": "en"},
"title": {"h1": "Onboarding in days, not months", "subtitle": "A proposal for Acme HR"},
"slides": [
{"pattern": "needs", "items": ["What Acme told us, in their words, with the source"]},
{"id": "P2.4", "edit": {"title": "1 portal for 12,000 employees"}},
{"chapter": "P3"},
{"pattern": "plan", "steps": ["Pilot in January", "Rollout in March"]}
],
"closing": {"tagline": "Happy to walk your team through it."}
}
The assembler is a plain Python script. It copies each slide's HTML from the master, applies the edits, fills the few patterns (title, needs, fit, plan, closing) from HTML templates, and writes 1 file.
The real gain is that a wrong plan stops the build. My assembler refuses an unknown slide id, an edit that matched nothing, media missing from disk, an internal slide in a client deck, the same slide twice, an untranslated string, and a colour typed into the plan. Each refusal is a bug that never reached a client.
Updates get cheap too. The plan stays beside the deck, so "drop slide 7 and move pricing up" is a 2-line edit and a rebuild.
3. No library? Write the slides, but only from the design system
Half my decks never touch the master: an account review for my manager, a yearly plan, an exec update, a 1-slide architecture view for a meeting. The library has nothing for them, so the model writes every slide.
It still doesn't write free HTML. It writes each slide from the design system in the theme:
- A fixed grid. 1920x1080, set margins, a gutter, a header and a footer zone. Every slide snaps to it.
- A short list of templates and layouts. Mine has 8 templates and 16 named layouts: split screen with a panel, 3 columns, stats, a 2x3 grid, a table, a plan with dated steps, a quote. Each one is a few CSS classes.
- No CSS per deck. When a slide needs a layout that doesn't exist, the layout goes into the theme once, and every later deck has it.
So the guardrail moves. In a client deck, the library guarantees the facts. In an internal deck, the design system guarantees the look, and the facts come from my notes, the CRM export or the call transcript the model reads first.
The plan stays the same file. It holds the written slides as HTML entries instead of slide ids, and it carries an audience flag:
| Audience | What the build allows |
|---|---|
client |
Library slides where they exist, no internal slides or numbers |
internal |
Internal slides, deal figures and their own checks |
Internal decks get their own checks. Mine writes every amount in US dollars, short form ($47k, $1.2M), and QA fails a slide that doesn't.
2 habits make these decks faster:
- Reuse past decks. A script indexes every slide of every deck I've built, with a short description, its purpose, its layout, and whether it's generic, to adapt or specific to 1 account. The model searches that index before it writes. A hit from another account's deck is a pattern to adapt, never a slide to copy: its name, logo and numbers go.
- Fold good slides back. When an internal slide proves useful twice, it becomes a pattern or joins the library, as its own task, not in the middle of a deck.
Everything after the plan is identical: the same assembler, QA, PowerPoint export and versions.
4. Spend the best model on the argument only
A deck has 2 kinds of work: judgement (what's the story, which pains are real) and volume (adapt 40 slides, translate 300 strings, check 20 screenshots). I split them by model:
| Step | Who | Why |
|---|---|---|
| Client brief from notes and calls | Strongest model, subagent | Which pains are real, with a source |
| Slide shortlist from the map | Mid-tier model, subagent | Ranked search, no judgement needed |
| The plan | Main session, never handed off | The plan is the deck |
| Light client wording on each chapter | Strong model, 1 per chapter, in parallel | Volume, reviewed before merge |
| Translation | Strong model, 1 per sheet, in parallel | Volume, with a translation memory |
| Assembly | Script | Deterministic |
| Visual check of screenshots | Mid-tier model with vision | It reports, I decide |
2 details made a big difference. Translations go through a memory: a string translated once comes back pre-filled next time, so only new strings cost tokens. And no client wording enters that memory, or it would leak into the next client's deck.
5. HTML is the source, PowerPoint is derived
I build in HTML because a model writes it well, it diffs, it plays video, and a browser can measure it. Each deck is 1 .html file at 1920x1080 on reveal.js. It links the engine, the theme CSS and the media by absolute path instead of copying them, so the file works from any folder and always shows the current theme.
People still ask for PowerPoint. The tempting shortcuts are both wrong:
- Screenshots in a
.pptxlook right and nobody can edit them. - A second, hand-written PowerPoint drifts from the HTML within a week.
So the .pptx is composed from the finished HTML. A converter reads each slide, turns it into a spec (template, title, bullets, images, video), and redraws it with python-pptx through 1 function per template, the archetypes. The result has real text boxes, real shapes and embedded video that still plays.
The grid is the trick that keeps both formats aligned. Archetypes are written in the deck's own pixels and converted once:
| Slide | 13.333 x 7.5 in, PowerPoint's 16:9 |
| 1 px | 1/144 in |
| 1 px of type | 0.5 pt |
| Brand tokens | Copied from the HTML theme, never retyped |
The rule for iterations: once a human edits the PowerPoint, the PowerPoint is the source. Regenerating it from the HTML would wipe those edits. My converter refuses to overwrite a .pptx newer than its HTML. Small changes go into the .pptx itself through find-and-replace edits, which survive moved shapes and keep the formatting. A structural change is a rebuild, and the person is told which edits it drops.
6. Check by measurement, then by eye
Models are bad at seeing their own layout bugs. Rules don't see them either. So QA has 2 passes.
Measurement. A headless Chrome loads every slide at 1920x1080 and checks geometry:
| Check | What it catches |
|---|---|
| FRAME | Anything outside the 1920x1080 canvas |
| SPILL | Text running out of its own box |
| OCCL | Text covered by another element |
| STACK | A picture drawn under a card |
| FOOT | Content crossing the footer line |
| GAP | A heading too close to its body |
It also flags type under 12 pt, images that didn't load, and every colour not in the brand palette. When I added the geometry pass, it found 55 broken slides out of 417 in the master that the structure checks had passed.
Eye. A script screenshots every slide, and a vision model reviews them against a named checklist: 1 accent per slide, nothing cropped, a visual on every slide, the logo where it belongs. It reports per slide. I decide.
The fix always goes into the plan or the template, then a rebuild. Never into the generated HTML: the next build would overwrite it.
7. 1 file name for life
Every deck gets 1 dated folder and 1 main file that never changes name. Before each rebuild, a script copies the current file to versions/ as the next -v1, -v2. The copy I have open in the browser or PowerPoint is always the latest, and nothing is ever lost.
Each build also stamps the title slide with a small YYMMDDHHMM mark. When a forwarded PDF comes back with questions, I know which generation it was.
How to build your own
- Pick the format. reveal.js at a fixed 1920x1080, 1 file per deck, with a shared theme CSS.
- Build the design system first. A grid, 5 to 8 templates and a handful of layouts in 1 theme CSS. Internal decks need nothing more.
- Build the library. For decks you give often, collect 20 to 30 slides you'd present without changes, on those templates. Give each slide an id.
- Generate the map. A script that prints id, template and headline for every slide.
- Define the plan schema. Slide ids, edits, a few patterns, written HTML slides, an audience flag. Write it down so the model reads it before every deck.
- Write the assembler. Copy slides, apply edits, fill patterns, and fail loudly on anything unknown.
- Add the QA loop. Playwright or Puppeteer for the measurements, DeckTape for screenshots and PDF.
- Add PowerPoint last. 1 python-pptx function per template, reading the built HTML.
- Wrap it in a skill. A Claude Code skill that says, in order: frame, gather, plan, assemble, QA, deliver. My skills setup is in see The NicAI skills catalogue.
Gotchas
- reveal.js fights CSS grid. It owns the slide's
displayand leaves its height undefined, so grid layouts collapse. Force the size on the present slide only, and use flexbox for vertical centring. - A live theme changes old decks. Every deck links the same CSS, so a theme change shows on the next reload. Export a PDF to freeze what you sent.
- Set a poster on every video. A PDF export shows the poster frame, not the video.
- No animations by default. They break the export and rarely help the argument.
To present, I use my own app, see Deckee: 1 window, 1 deck, fullscreen on the second screen.
Further reading
The HTML framework every deck runs on:

The Python library behind the native PowerPoint export:

PDF export and slide screenshots from any HTML deck:
