Back to home
Apisurf Logo
deck

deck

Markdown slides as a plain JSON list, turned into a presentation you can stand in front of.

What is deck?

A presentation is a JSON list of markdown slides. That is the entire content model.

presentation.json
[
  "# Quarterly review\n\nOctober",
  "## Where the time went\n\n- Migrations\n- Meetings\n- Actual work"
]

Two commands work on it. deck writes decks and is built for an agent to drive: every command is one edit to one record, and a deck is a folder on disk — no database, no server, no account. deckui renders them, and never writes.


Installation

bash
npm i -g @apisurf/deck      # writing
npm i -g @apisurf/deckui    # building and serving

Published to GitHub Packages under the @apisurf scope, so installing needs a token in your ~/.npmrc:

~/.npmrc
@apisurf:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=<a PAT with read:packages>

They are separate packages so an agent that only writes slides never installs Astro, marked or mermaid.


Five minutes

bash
deck new deck "Quarterly Review"         # slug quarterly-review, title on slide 1
deck new slide --deck quarterly-review --file ./where-the-time-went.md

deck img add ~/Desktop/revenue.png --deck quarterly-review
deck new slide --deck quarterly-review --content "![Revenue in Q3](images/revenue.png)"

deck ls slides --deck quarterly-review   # the slides, numbered
deckui serve quarterly-review            # build it and open it in a browser

Where decks live

In one global folder, not per project, so every shell and agent session sees the same decks. Each deck is a folder named by its slug; there is no index, so a folder copied in is a deck and one deleted is gone.

~/.apisurf/deck
~/.apisurf/deck/
  quarterly-review/
    presentation.json
    images/
      revenue.png
  arch-talk/
    presentation.json
    images/

Point any command elsewhere with --home <dir>, or $DECK_HOME for a whole session.


The format

presentation.json is a JSON array of strings. One string is one slide, written in markdown; the order of the array is the order of the talk. There is no wrapper object, no ids and no per-slide settings. Four things follow from that:

What followsWhy
The title is the first heading of the first slideThere is nowhere else to put one. deck new deck writes the name there, so retitling is deck set slide 1
Slides are addressed by position, from 1A slide has no identity beyond where it sits, so deck mv slide changes what deck set slide 3 means
A slide opening with an # heading is centredAs a title or a section break. Everything else is flush left
Anything that can edit JSON can edit the deckThe commands are a convenience over an array, not a database with a shell around it

Markdown is GitHub flavoured. Two things are worth calling out, both of which are just markdown:

markdown
![Revenue by quarter](images/chart.png)

```mermaid
flowchart LR
  deck[presentation.json] --> build[deckui build] --> serve[deckui serve]
```

Images live in the deck's own images/ folder and are linked relatively, so one link works in a build served at the root and under a sub-path. A fenced block whose language is mermaid is drawn as a diagram; a deck with no diagram in it never loads the library.


Commands

CommandDoes
deck new deck "<name>"Create a deck. --slug picks the slug instead of deriving it from the name
deck new slideAppend a slide, or insert it before --at <n>
deck ls decks|slidesThe decks in the store, or one deck's slides, numbered
deck get deck|slide <ref...>A deck's facts, or a slide's markdown
deck set slide <ref>Replace a slide
deck set deck <slug> --slug <new>Rename a deck
deck mv slide <ref> --to <n>Reorder a slide. --into <deck> moves it to another deck, without its images
deck rm slide <ref...>Delete slides
deck rm deck <slug...>Delete decks. A non-empty one needs --force; --all --force empties the store
deck cat <ref...>The markdown of a whole deck, or of the slides named
deck img [add|rm]List, add or delete a deck's images
deck howto [name]Worked examples: write, many, images, diagram
deck help [command]One command's page. Topics: refs, store, format, images

And on the other side

CommandDoes
deckui ls [decks|slides]What is in the store, and which decks are built
deckui build [deck]Render into static HTML, in ./dist/<slug> unless --out says otherwise
deckui serve [deck]Build into .builds/<slug> in the store, serve it and open the browser
deckui open [dir]Serve a folder that is already built, without reading the store
deckui howto [name]Worked examples: present, publish, snapshot
deckui help [command]One command's page. Topics: keys, snapshot, store

A deck is named by its slug. A slide is its position, or last, and its deck comes from --deck or the ref itself (q3/3); when the store holds a single deck, neither is needed. A slide's body comes from exactly one of --content, --file <path>, or a bare - for stdin.

bash
deck set slide 3 --deck q3 --content "## A heading"
deck set slide q3/3 --file ./slide.md
printf '# Questions?' | deck new slide --deck q3 -
deck rm slide last --deck q3

Typing one tool's command at the other is answered rather than refused: deck build prints the deckui line with the flags unchanged, and deckui new points back at deck.


The built deck

deckui build writes one HTML page, a stylesheet, a small script, the diagram library if a slide needs it, and images/. Every slide is in that one page, so any static file server can host it and it needs no network once loaded.

keys
→ ↓ Space PageDown      next slide
← ↑ Backspace PageUp    previous slide
Home  End               first, last
F                       fullscreen

The URL carries the slide number, so a reload keeps your place, a link can point at one slide, and the back button steps backwards through the talk. Printing the page gives one slide per sheet, which is how you get a PDF. There are no transitions, no presenter view, no speaker notes and no timer.

deckui serve and open listen on 127.0.0.1:4200, or the next free port; --port and --host change that, and serve --no-open skips the browser.

bash
deckui build q3 --out ./public --base /talk               # for a sub-path on a static host
deckui open ./public --base /talk                         # check it before uploading
deckui serve q3 --host 0.0.0.0                            # present to a screen on the network
deckui build q3 --snapshot-only --snapshot ./deck.json    # the JSON, for another renderer