Published:
Tagged: JavaScript Node Open-Source Presentations Public-Speaking
TLDR; sipario is the engine behind one of my talks, pulled out as a standalone library. A talk is a folder:
deck.md, its own templates, its own CSS. sipario supplies the format, the renderer and its checks, the presenter window and a server that reloads as you save. It ships no design of its own. No dependencies, MIT,npm install sipario.
![]()
I wrote a deck engine because I wanted a conference talk to be one markdown file. A # line opens a movement, a ## 3.2 A title line opens a slide, -- starts the next step of a build, and a fenced notes block is what I say over it. The server renders the file on every request, so nothing on screen can be a stale copy of something I’ve since edited.
Writing it for exactly one talk is why it worked. Taking it out is what showed me which half was the engine.
Three questions, and none of them were visible while the engine and the talk lived in one repo.
Where does a library’s talk come from? It doesn’t. render(src) used to render whatever sat in ./talk, via a module-level default. That’s the worst shape a failure can take: it doesn’t crash, it succeeds, against somebody else’s slides. Now the template and image roots are required, and a missing one throws an error naming which.
Who is this deck? One talk’s title was a string inside server.js. Its name was the sessionStorage key in deck.js. The same string again was the BroadcastChannel the deck and the presenter window talk over, in two more files. Three copies of one talk’s identity, sitting in code every other talk would read. Now a deck opens with name: and says who it is — required, not defaulted, because a default is exactly how two decks come to be called the same thing, and two decks served from one origin sharing a channel means one deck’s arrow key moves the other.
Does the engine know any template by name? It did, in one line: if the template was called section and had no word, use the movement’s name. One line, and it meant the engine knew something about a template only my talk had. Now the movement’s name reaches every slide as meta.group and the talk’s own template is the thing that says a template called section is named by the movement it opens. The same rule caught the presenter window hunting for a slide’s heading with a selector that named three more of my templates.
The old suite was 1,952 lines over a repo holding both an engine and a talk. Splitting it turned out to be three piles, not two: what’s true of sipario, what’s true of my talk, and what’s true of any talk — templates compile, every class a template writes has a rule and every rule a class, every figure the deck names is on disk, every step has a script.
That third pile ships as talkChecks(), so a talk runs those rules over its own folder rather than keeping a copy of them. A copy in two repos is the first of them going stale, which is most of what this extraction existed to prevent.
Running them over the shipped example for the first time found a real bug. The example’s own stylesheet had never carried the rules for fourteen classes its own templates write. The folder a stranger copies to start had been wrong the whole time, and only became visible once the rules stopped being one talk’s private business.
There are no themes, and there is no default look. A folder with no templates/ is refused by name rather than quietly handed somebody else’s design, because what a slide looks like is the talk’s decision and guessing on its behalf is the engine making it. Two starters come in the box — minimal, and stylish, the same deck set like the bill outside an Italian theatre — and both carry their look entirely in deck.css and templates/.
The other thing it refuses is guessing about your slides. Ids and numbers are declared and then verified, never derived: a number that disagrees with where the slide actually sits stops the render and names both. So does a standfirst on a template that never prints one, or a step restating a line already on the stage. A slide that renders plausibly and wrongly is worse in a room than one that never rendered at all.
mkdir my-talk && cd my-talk
npm init -y
npm install sipario
npx sipario new # a talk in ./talk, named from the folder
npx sipario serve # http://localhost:9999
Node 22 or later, and nothing else — the server is Node’s own http. No build step, so there’s no artefact to go stale. No PDF export, no plugin API.
This is 0.1.1 and the first release, so it’s early. MIT, at github.com/pardel/sipario.
sipario is Italian for the theatre curtain, the one that rises to open a performance.