PowerPoint files are binary blobs. You can't diff them, you can't review them in a
pull request, and you can't reuse the code that already lives three folders over in
the same repo. So every deck gets rebuilt from scratch, and the good one is always
readout_final_v3_REAL.pptx in somebody's inbox.
This repo is a working alternative. A deck is one Markdown file. Styling lives in one CSS file. A build step turns them into a website, a PDF, or a single HTML file you can email to someone who will double-click it and never know the difference.
Read decks/decks-as-code/slides.md to see it. That file is a deck about this,
written in this. It is published at
https://ivorb.github.io/decks-as-code/ by the build in this repo.
No build step, no npm install. Serve the repo over HTTP:
cd decks-as-code
python -m http.server 8931
# then open http://localhost:8931/Serving it matters. Opening index.html straight off disk won't work, because the
pages fetch decks.json and slides.md and browsers block that on file://. The
built standalone/ files are exempt, since everything is inlined into them.
Controls: ←/→ navigate, ↓ vertical slides, S speaker notes,
Esc slide overview, F fullscreen, ? all shortcuts.
decks-as-code/
├── index.html landing page, lists every deck
├── deck.html the player: deck.html?d=<slug>
├── decks.json generated listing, don't hand-edit
├── assets/theme.css shared brand styling for all decks
├── tools/
│ ├── build-manifest.mjs regenerates decks.json
│ ├── build-site.mjs builds dist/: offline site plus single-file decks
│ └── import-pptx.mjs converts a .pptx into a deck folder
└── decks/
└── <slug>/
├── slides.md the deck. The only file you edit day to day.
├── deck.json title, description, author, date, tags, theme
└── images/ optional, referenced as 
| URL | Serves |
|---|---|
/ |
Landing page listing all decks |
/deck.html?d=<slug> |
That deck, full screen |
/decks/<slug>/deck.pdf |
PDF export, once built |
mkdir decks\my-talk
# write decks\my-talk\slides.md, and optionally decks\my-talk\deck.json
node tools\build-manifest.mjsIt shows up on the landing page, at /deck.html?d=my-talk.
deck.json is optional. Without it the deck is titled from its first Markdown
heading. The fields:
{
"title": "My Talk",
"description": "One line for the landing page card.",
"author": "Your Name",
"date": "2026-09-01",
"tags": ["readout"],
"theme": "night"
}theme is any stock Reveal.js theme (night, black, white, league,
solarized, and so on). assets/theme.css layers brand styling over whichever you
pick.
You do not have to retype decks you already have. Open the site and click Import a .pptx on the home page, or use the command line:
node tools\import-pptx.mjs "C:\path\to\talk.pptx" --slug my-talk
node tools\build-manifest.mjsA .pptx is a ZIP of XML, so this reads the file directly. No PowerPoint, no
network, no dependencies.
Both routes run the same converter, so they produce the same slides.md. The
browser version does the whole conversion locally — your file is never uploaded —
and lets you preview the result before you keep it. From there you can download a
ZIP to unpack into decks/, or, in Chromium browsers, write straight into your
decks/ folder. The import page needs to be served (see below); it will not work
if you open it from disk.
What comes across:
| Recovered | Notes |
|---|---|
| Titles | From the title placeholder |
| Bullets | Nesting levels preserved |
| Bold, italic, links | Inline formatting |
| Tables | Converted to Markdown tables |
| Code | Monospace text boxes become fenced code blocks, indentation intact |
| Images | Written to decks/<slug>/images/, SVG preferred over raster |
| Speaker notes | Become Note: blocks |
| Slide order | Read from the presentation, not filename order |
What does not, because it is drawing instructions rather than content: layout and positioning, animations and transitions, charts, SmartArt, themes and masters. The importer lists the slides it could not fully handle when it finishes.
Treat the output as a first draft. A slide deck built for narration usually has more text on it than one built for a repo, so expect to cut.
Options:
| Flag | Effect |
|---|---|
--slug <name> |
Folder name (defaults to the filename) |
--title, --author, --date, --theme |
Override deck.json fields |
--no-images |
Text only, skip the media |
--dry-run |
Print the first 40 lines, write nothing |
--force |
Overwrite an existing deck folder |
--out <dir> |
Write somewhere other than decks/<slug> |
slides.md is ordinary Markdown with three separator conventions:
| Syntax | Meaning |
|---|---|
--- on its own line |
New slide |
-- on its own line |
New vertical sub-slide: optional depth you can skip live |
Note: |
Everything after it becomes a speaker note (press S) |
Fenced code blocks get real syntax highlighting. <pre class="mermaid"> blocks
render as diagrams, so there are no image files to keep in sync with the thing they
illustrate.
Per-slide styling uses HTML comment attributes:
<!-- .slide: data-background-color="#1a1a2e" -->node tools/build-site.mjs produces dist/:
| Output | What it's for |
|---|---|
dist/standalone/<slug>.html |
A whole deck in one file. Double-click to open. No server, no internet. The easiest thing to email or paste into a chat. |
dist/ |
The full site with every dependency vendored. Serve it from any static host or file share. Makes no external requests. |
dist/decks/<slug>/deck.pdf |
PDF export. |
standalone/ is the only part you can double-click. index.html and deck.html
read decks.json and slides.md at runtime, and browsers block those reads on a
file:// URL, so those two pages need a server even after they are built. Open them
from your file system and they now say so rather than failing silently. To run the
whole site locally, serve dist/ on a port as shown under "Run it locally".
.github/workflows/publish-decks.yml runs on every push and pull request. It builds
dist/, exports a PDF per deck, and uploads two artifacts: deck-standalone and
deck-pdfs. On main it also deploys dist/ to GitHub Pages.
GitHub Pages. This repo is public, so Pages is enabled and every push to main
republishes https://ivorb.github.io/decks-as-code/. Settings → Pages → Source is set
to "GitHub Actions".
Sharing from a private repo. If you copy this into a private repo, Pages needs a
paid plan. Artifacts still work: open the latest workflow run, download
deck-standalone, unzip, and open the HTML. Anyone with repo access can do the same.
Add a deck without running build-manifest.mjs and the --check step fails the
build, rather than quietly leaving your deck off the listing.
Reveal.js, highlight.js, and Mermaid load from jsDelivr at exact pinned versions, set
as constants at the top of deck.html. Floating majors like reveal.js@5 are
avoided on purpose: a minor release can change how an already-approved deck renders.
To upgrade, bump the versions in deck.html and tools/build-site.mjs together, then
look at the decks.
They load as classic scripts rather than ES modules, also on purpose. decktape drives
the global Reveal.initialize() API to export PDFs, and plain script bundles can be
copied into dist/vendor/, while Mermaid's ESM build fetches lazy chunks that can't.
build-site.mjs downloads those exact files into dist/vendor/ and caches them in
.asset-cache/, so the built output needs no CDN access at all. Handy if jsDelivr is
blocked on your network. It also strips the Google Fonts imports that ship inside the
Reveal themes, since assets/theme.css replaces both font stacks anyway.
- Reviewable. Changes are a readable text diff in a PR, not a 4 MB attachment.
- Reusable. Code, configs, and README snippets already in the repo go straight into a slide instead of being screenshotted.
- Consistent. Branding is
assets/theme.css. Restyle every deck by editing one file. - Durable. Plain text still opens in ten years, and it greps.
- Live code. Highlighted, copyable, and correct, instead of a blurry screenshot of somebody's editor.
- Placing a free-floating element exactly where you want it is harder than dragging a box in PowerPoint. This suits structured, content-driven decks better than design-led ones.
- Non-technical contributors need a Markdown on-ramp. A template deck and a web editor (github.dev works) covers most of it.
- Someone has to own the theme CSS. Budget a small one-time setup.
- PDF export runs headless Chrome. Automated here, but it's one more moving part.
- PDF. Every deck gets one from the build. Locally:
npx decktape reveal "http://localhost:8931/deck.html?d=<slug>" deck.pdf - Single file.
node tools/build-site.mjswrites a self-containeddist/standalone/<slug>.html. - PPTX. If someone genuinely needs a
.pptx, run that deck'sslides.mdthrough Marp:npx @marp-team/marp-cli slides.md --pptx - Hosting. It's static HTML. Any file share, intranet host, or GitHub Pages.
| Tool | Best for | Effort | Native PPTX export |
|---|---|---|---|
| Marp | Simplest thing that works. Markdown in, PDF or PPTX out. | Lowest | Yes |
| Reveal.js | Most control, custom HTML and CSS. This setup. | Low to medium | No, PDF only |
| Slidev | Polished themes, live coding, developer audiences | Medium | No, PDF or PNG |
If the main worry is being able to back out later, pilot Marp. If it's control and a
distinctive look, stay on Reveal.js. The slides.md files here are close to portable
between the two, mostly differing in front-matter and separators.
- Pilot with one real deck. This one, or an upcoming team readout.
- Put brand colors, fonts, and a title-slide layout into
assets/theme.css. - Create a
deck-templaterepo so starting a deck is one click. - Share builds through the workflow artifacts, or enable Pages if the repo goes public.
- Get feedback after two or three decks before pushing it org-wide.