AsciiDoc Slides: One Gem, One Command

Earlier today I wrote about making Claude write decks as markdown and letting Marp do the layout. The argument holds — but Marp is a node install, and markdown’s vocabulary is deliberately tiny. If you already have Ruby on the box, and you want fragments, vertical slides and two-column layouts without hand-rolling HTML, the other end of the same idea is AsciiDoc: asciidoctor-revealjs.

The whole toolchain

$ gem install asciidoctor-revealjs rouge

That’s it. The gem has exactly one runtime dependency — asciidoctor itself — and rouge is optional (more on it below). No node, no browser, no LaTeX. It installs into your per-user gem dir, which is the folder you already put on your PATH.

Then:

$ asciidoctor-revealjs deck.adoc
$ xdg-open deck.html

0.15 seconds for an 8-slide deck. file:// is fine — no server needed.

Is it worth trusting?

I check this before every gem install, and this one comes out clean:

  • Lives in the official Asciidoctor org, MIT licensed, 313 stars.
  • 26 releases since 2016, currently 5.2.0, with 6.0.0-beta.2 cut three days ago. Not abandoned, not churning.
  • 296k downloads, and 4 open issues.
  • One runtime dependency. Nothing to audit.

The deck is one file

= No Tools, More Slides
Martin Vysny
:revealjs_theme: white
:revealjs_slideNumber: c/t
:revealjsdir: https://cdn.jsdelivr.net/npm/reveal.js@5.2.1
:source-highlighter: rouge
:experimental:

== Why AsciiDoc slides?

* Slides are *plain text* -- diffable, greppable, git-friendly
* One `.adoc` file in, one `.html` out

== Lists reveal one by one

[%step]
* First this
[%step]
* then this

== Code, highlighted

[source,ruby]
----
ARGF.each { |line| puts line unless line.strip.empty? }
----

Press kbd:[S] for speaker notes, kbd:[ESC] for the overview.

[.notes]
--
This is a speaker note -- only the speaker window shows it.
--

== Vertical slides

Sub-sections nest *downwards*: press the down arrow.

=== Down here

`==` makes a new slide to the right, `===` one below it.

== Two columns

[cols="2*a",frame=none,grid=none]
|===
|
The whole toolchain:

* one gem
* one command

|
image::pipeline.svg[Pipeline,420]
|===

Every line in that header does one thing, and the body is just AsciiDoc. == is a slide, === is a slide below it (reveal.js gives you exactly two axes — right and down, no deeper). [%step] on a list reveals it item by item. [.notes] is the speaker note. A two-column layout is an AsciiDoc table with no frame and no grid, which sounds like a hack until you notice you never have to touch a <div>.

That’s the difference from markdown, and it cuts both ways: a bigger vocabulary buys you layouts, and it also buys you enough rope to write a bad slide.

Four things that will bite you

:revealjsdir: decides where reveal.js comes from. Leave it out and the generated HTML points at a local reveal.js/ directory that doesn’t exist — you get unstyled text and no idea why. Point it at a pinned CDN version like above, or download reveal.js once and build against it:

$ curl -L https://github.com/hakimel/reveal.js/archive/refs/tags/5.2.1.tar.gz | tar xz
$ mv reveal.js-5.2.1 reveal.js
$ asciidoctor-revealjs -a revealjsdir=reveal.js deck.adoc

7 MB on disk, and then every asset in the deck is local — which is what you want for the conference wifi.

kbd:[S] needs :experimental:. Without that one header line, the keyboard macro doesn’t error — it renders the literal string kbd:[S] onto your slide. Same for btn:[] and menu:[]. I only caught it by looking at the rendered page.

rouge is a separate gem. :source-highlighter: rouge without it prints WARNING: optional gem 'rouge' is not available and silently drops all highlighting. It’s worth installing, because rouge highlights at build time — the alternative, highlight.js, ships a CDN script that highlights in the browser on every load. Server-side is one less runtime dependency in a file you’ll open on someone else’s laptop.

Keep code lines short. There’s no wrapping and no auto-shrink: a long line in a [source] block just runs off the right edge of the slide, and you find out in front of the room. My first draft lost half a one-liner that looked fine in the editor.

If you do need PDF

The gem only emits HTML — PDF means printing that HTML with a browser, and the tool that does it properly is decktape:

$ PUPPETEER_EXECUTABLE_PATH=/snap/bin/chromium \
  npx --yes decktape reveal http://127.0.0.1:8765/deck.html deck.pdf

One page per slide, 960x540 pt, fragments flattened. The env var stops puppeteer downloading a second Chrome when you already have one. Two things that don’t work: decktape against a file:// URL (serve the directory — ruby -run -e httpd . -p 8765 is already on your box), and Chromium’s non-interactive --headless --print-to-pdf, which produced a single blank page for me every time. If you want it by hand, deck.html?print-pdf plus Chrome’s own print dialog is the documented route.

Why I like this one

The Marp post ended on the format is the prompt, and that’s still the point. What AsciiDoc changes is who’s holding the tools: no node in the tree, no watcher process, no export step — a text file, a gem, and a browser tab I refresh myself. The build is fast enough that a refresh is the loop.

And the deck ends up being the only artifact I own. deck.adoc is 1.3 kB and diffs like source, because it is source.

Written on August 20, 2026