14 Jun 2026
After surveying the field (HTML Presentations for Agentic Workflows), I decided not to adopt an off-the-shelf deck tool and instead build a thin system of my own on top of reveal.js. The survey is the reasoning: reveal.js is the safest target for agent-generated slides, so I wrap it in an opinionated layer that fits how I actually work, and let Claude generate the decks. I call it NicDeck.
The shape in one sentence: every presentation is a single .html file I can move anywhere, the engine and the design theme live in one central place and are referenced (not copied) so every deck always uses the latest design, and the graphics and video stay put in a central media library, linked by absolute URL.
For the engine itself, see the deep-dive: reveal.js
Goals and constraints
What the system has to do:
- Each deck is one self-contained
.htmlfile, movable anywhere on disk. - The runtime (reveal.js engine + my theme) lives centrally and is referenced, so editing the theme updates every deck.
- Content assets (graphics, video) stay in place in a central media library, referenced by absolute links, never copied into the deck.
- Symlinks make the central runtime a stable facade over the real source files.
- Fixed 1920x1080, presented on a 1080p monitor. No speaker view.
- Full design freedom: a blank-slate theme. reveal supplies behaviour, zero visual opinion.
- Five slide templates: title, section, product-overview, feature, content.
- In-slide animations via reveal fragments.
- Navigation: linear, vertical (nested sections), and the overview grid.
- Export to PDF and PPTX.
Architecture
The trick that makes "single movable file + central assets + working video" work is a small local web server. Every deck references the runtime and media by absolute http://localhost:7000/... URLs, so the links never change when I move the .html around.
| Constraint | Mechanism |
|---|---|
| Single movable file | All slide markup inline; only shared deps are external |
| Move anywhere | Deps referenced by absolute localhost URLs |
| Latest design everywhere | _runtime/deck.css is a symlink to the one theme |
| Central media, in place | Absolute localhost URLs into the media library |
| Video that seeks | Served over HTTP, which gives range requests |
The deck's own URL changes when I move it, but its internal links are absolute and do not, so it keeps working from any location under the served root (my home folder).
Layout
~/ai/ka/skills/deck/ # the skill: engine, theme, templates, CLI, export
~/ai/ka/decks/
├── _runtime/ # symlinks -> the skill (reveal.js, reveal.css, reset.css, deck.css)
└── <client>-<topic>.html # the decks: loose single files
~/ai/ka/assets/media/
└── <client>-<topic>/ # per-deck graphics + video, absolute-linked
Baked constants: port 7000, served root $HOME, URL base http://localhost:7000. They are written into each deck at creation, so they stay stable. Changing them only affects decks made afterwards.
The local server
A caddy file-server runs under launchd, bound to 127.0.0.1:7000, rooted at my home folder. caddy gives correct MIME types and proper HTTP range support, which video needs to stream and seek. A Python http.server is the zero-install fallback.
Why a server at all, instead of opening the file directly: Chrome blocks a file:// page from loading resources in other directories (the central runtime and media are cross-directory), and <video> needs HTTP range requests to seek, which file:// does not provide. Binding to 127.0.0.1 keeps it off the network.
The design system
One file, deck.css, is the only place with real design work and the single source of truth. Decks compose from its tokens, template layouts, and fragment classes. They never carry bespoke per-deck CSS. A new visual need is added here once and propagates everywhere through the symlink.
It is built against the 1920x1080 canvas: design tokens for colour, type scale, and spacing, then a layout per template, then the animation vocabulary.
:root{
--bg:#0b0b10; --fg:#f4f5f7; --accent:#4f7cff;
--font-sans:"Inter", system-ui, sans-serif;
--fs-display:132px; --fs-h1:76px; --fs-h2:52px; --fs-body:30px;
--pad:110px; --gap:40px; /* designed against 1920x1080 */
}
/* every slide fills the canvas; reveal supplies behaviour only */
.reveal .slides > section{ width:1920px; height:1080px; padding:var(--pad); }
Fragments get a small animation vocabulary (fade-up, reveal-left) layered on reveal's .fragment.
Slide templates
Five reference patterns the deck-authoring agent copies and fills:
- title: eyebrow, headline, value-prop subtitle.
- section: chapter divider.
- product-overview: split layout, also the example of vertical drill-down (a nested
<section>you press down into for detail). - feature: full-bleed split with a looping video demo.
- content: general body slide with staged bullets.
Vertical nav comes from nesting <section>s; the overview grid is automatic. A content slide with staged reveals:
<section data-template="content">
<h2>Heading</h2>
<ul>
<li class="fragment fade-up">Point one</li>
<li class="fragment fade-up">Point two</li>
</ul>
</section>
The deck skeleton loads reveal as a classic script and initialises it for the fixed target: width:1920, height:1080, center:false, overview:true, fragments:true.
Export
One CLI, deck, wraps open and export:
deck open <file>opens the deck at its localhost URL in Chrome.deck pdf <file>runs decktape against the URL for a real vector PDF (selectable text, embedded fonts).deck pptx <file>renders each slide to a 1920x1080 PNG with decktape, then assembles one full-bleed image per 16:9 slide with PptxGenJS.
The PPTX is image-per-slide: recipients can reorder and add slides but cannot edit the text. That is the right trade for "send me the deck in PowerPoint." Truly editable native PPTX would need a different architecture (one structured content object rendered to both reveal and native shapes) and is out of scope for v1.
Generating a deck
This is where it stops being a CSS framework and becomes a skill. A deck is built by a master orchestrator agent with subagents for each job, and it goes through brand and Council review on every run.
The flow:
- Identify the source material. Find the relevant Kaltura master decks (the PowerPoint masters) and pick the slides that fit the task.
- Gather product knowledge from the KB, scoped to the task.
- Gather client knowledge from the KB and the databases when the deck is for a named client.
- Build the deck:
- Almost every slide carries a relevant graphic. Text-only slides are the exception.
- The Kaltura logo is on every slide.
- For a client deck, the client logo goes on the title slide.
- Every path to a graphic or video is absolute (starts with
/Users/nic/...), because the.htmlgets moved around. - Run it past the Council. Loop, up to 10 times, feeding each expert's feedback back into the deck, until every expert scores it at least 9/10 from their own angle.
- Visual QA. A web and presentation-design expert checks the rendered deck looks professional, with clean layout, before it ships.
The brand skill enforces the visual identity, the Council is the quality gate, and the orchestrator keeps the subagents pointed at one deck.
Known constraints
Deliberate, not bugs to fix:
- The server is mandatory.
file://cannot do cross-directory assets or video seeking. - Movability is bounded by the served root. A deck works from anywhere under my home folder; move it outside and the links break.
- Live, not frozen. The HTML always renders the current theme, so editing
deck.csscan restyle a deck I already shipped. To freeze one for delivery, export it to PDF or PPTX. - Exports are static: video shows its poster frame, so every
<video>needs aposter. - The server binds
127.0.0.1only, never the network.
Status
Building v1 now. The system is documented as a deck skill under ~/ai/ka/, so Claude can author a deck end to end: pull the source slides and knowledge, compose from the templates, brand it, and run the Council loop until it clears.
This came out of the wider survey of agent-generated presentation tools: HTML Presentations for Agentic Workflows