A zero-config, fully reproducible presentation environment powered by Marp and Nix.
- Write slides in Markdown
- Build pixel-identical HTML, PDF, and cover images anywhere
- Forget toolchain drift forever
Presentations have a tooling problem. Proprietary editors lock content into binary formats. Web slide builders die when their SaaS goes offline. Even "plain HTML" decks tend to drift the moment a teammate runs a different Node version, a different Chromium, or a different font fallback.
This repository is a deliberate, opinionated answer to that problem:
- Markdown-first authoring: Slides are text. They diff cleanly, review like code, and survive any tool you'll ever use.
- Marp for rendering: A mature, fast, themeable Markdown-to-slide engine with a real CLI, live server, and PDF export.
- Nix flakes for the toolchain: The Marp CLI, the headless browser used for PDF rendering, the emoji set, and every supporting binary are pinned by hash. Anyone with Nix can reproduce your deck, byte-for-byte, five years from now.
- Convention over configuration: Drop a
.mdfile underslides/, get HTML, PDF, and a PNG cover. No project per deck, no boilerplate, no scripts to copy.
The combination is greater than the sum of its parts. Marp gives you authoring; Nix gives you guarantees. Together they deliver a presentation pipeline you can hand to a colleague, a CI runner, or your future self without a single "well, it works on my machine..." moment.
If you write talks, lectures, internal briefings, or conference material, and you care that they still build cleanly next year: this is for you.
You need exactly one thing: Nix with flakes enabled. That's it. No Node, no npm, no Chromium, no font installer.
The easiest way to install Nix is through the Determinate Systems installer (flakes are on by default):
curl -fsSL https://install.determinate.systems/nix | sh -s -- installAlternatively, use the official installer and enable flakes manually by adding the following to ~/.config/nix/nix.conf:
experimental-features = nix-command flakes
This repo ships an .envrc containing use flake. If you install direnv and nix-direnv, your shell automatically enters the project's environment the moment you cd into the directory.
# Install direnv (example using Nix profile, do what best suits you)
nix profile install nixpkgs#direnv nixpkgs#nix-direnv
# Hook direnv into your shell (bash example)
echo 'eval "$(direnv hook bash)"' >> ~/.bashrc
# In the repo root:
direnv allowFrom then on, cd-ing into the project replaces nix develop entirely.
The flake targets:
| System | Status |
|---|---|
x86_64-linux |
Supported |
aarch64-linux |
Supported |
aarch64-darwin |
Supported |
Note
Darwin x86_64 support is expected to be dropped in NixOS 26.05, hence we chose to not support it explicitly, but it should work anyway if you add it to the flake's supported systems.
# 1. Clone
git clone https://github.com/nicolas-goudry/marp-deck-directory
cd marp-deck-directory
# 2. Enter the reproducible dev shell
nix develop # or simply `cd` here if direnv is set up (see previous section)
# 3. Start the live-reload Marp server
serveOpen http://localhost:8080: Marp serves an index of every deck under slides/ and reloads on save.
To create your first deck, drop a Markdown file under slides/ (see Authoring slides) and the server picks it up instantly.
.
├── flake.nix # Nix flake: devshell + per-deck packages
├── shell.nix # Devshell definition (marp-cli)
├── treefmt.nix # Formatter config (nixfmt + prettier)
├── .marprc # Marp CLI config: default theme paths, HTML tags enabled
├── .envrc # direnv hook (`use flake`)
│
├── pkgs/ # Custom Nix packages
│ ├── slides.nix # The build engine: discovers and builds every deck
│ ├── brave-unsandboxed.nix # Brave wrapper for PDF/cover rendering
│ └── twemoji.nix # Pinned Twemoji asset set
│
├── assets/ # GLOBAL assets, shared by every deck
│ ├── css/ # Custom CSS files
│ │ ├── theme.css # Example main custom theme (declares @theme custom-theme)
│ │ └── fonts.css # Example Ubuntu Sans / Ubuntu Mono @font-face declarations
│ ├── themes/ # Third-party themes
│ ├── fonts/ # Font files
│ └── images/ # Images
│
└── slides/ # YOUR decks live here
├── chicken-cooking.md # Flat deck
├── chicken-cooking.cover.md # Optional dedicated cover for chicken-cooking.md
│
└── 2026/q4-keynote/ # Self-contained deck
├── slides.md # Conventional name
├── cover.md # Optional cover
└── assets/ # Deck-specific assets
Two kinds of assets:
| Layout | Where | When to use |
|---|---|---|
| Global assets | assets/ at the repo root |
Logos, fonts, themes, anything reused across multiple decks |
| Deck-specific assets | <deck-dir>/assets/ |
Diagrams, screenshots, one-off images for a single deck |
Two kinds of decks:
| Style | Filename | Output name |
|---|---|---|
| Flat deck | slides/foo/bar.md |
foo-bar.{html,pdf,cover} |
| Self-contained deck (recommended for non-trivial talks) | slides/foo/bar/slides.md |
foo-bar.{html,pdf,cover} |
A self-contained deck owns its directory: it can carry a cover.md and an assets/ subfolder. Flat decks are perfect for short, asset-light talks.
Warning
Naming collisions
Don't put both foo/bar.md and foo/bar/slides.md in the tree: the nested form wins and the other is silently skipped.
Pick one style per deck.
This is where the project earns its keep. Everything that can break across machines (version drift, missing binaries, font fallbacks, sandboxing quirks) is removed from the table.
The devshell (shell.nix) declares exactly one input: marp-cli from a pinned nixpkgs revision (flake.lock).
Whoever runs nix develop gets the same Marp version you developed against, with no global Node install required.
Marp uses a real Chromium-based browser to render PDFs and PNG covers. Stock Chromium in sandbox-restricted environments (CI, containers, NixOS without setuid) fails non-obviously. The custom pkgs/brave-unsandboxed.nix derivation wraps Brave with the right flags:
--no-sandbox
--disable-setuid-sandbox
--disable-gpu
--disable-dev-shm-usage
Then, the build system passes the wrapper to Marp via --browser=chrome --browser-path=….
PDF and cover builds Just Work™ in any Nix environment, including stripped-down CI runners.
Emoji are rendered via Twemoji, pinned to v14.0.3 by sha256 hash in pkgs/twemoji.nix.
The 2K+ SVG asset set is copied into assets/twemoji/ at build time and referenced through Marp's options.emoji.twemoji.base setting.
Emoji look the same on Linux, macOS, and the colleague who only opens the PDF.
assets/fonts/ ships any font you drop in it (by default the full Ubuntu Sans and Ubuntu Mono families). assets/css/fonts.css declares the matching @font-face rules.
Decks render identically whether the viewer has these fonts installed or not, and PDF exports embed them.
Because every input is content-addressed by Nix:
- A teammate who runs
nix developgets the exact Marp, Brave, Twemoji, and font set you used. - CI builds with
nix build .#slidesproduce reproducible artifacts. - Future-you, two laptops from now, rebuilds the same deck pixel-for-pixel.
If a deck builds today, it builds forever.
mkdir -p slides
cat > slides/hello.md <<'EOF'
---
marp: true
theme: gaia
paginate: true
---
# Hello, Marp!
A slide about nothing in particular.
---
## Second slide
- Markdown lists
- Render as bullets
- Obviously
EOFRun serve (or marp --server .) and open http://localhost:8080: your deck is live.
| Feature | Syntax |
|---|---|
| Slide separator | --- on its own line |
| Per-slide directive | <!-- _class: lead --> (single slide), <!-- class: lead --> (sticky) |
| Speaker notes | <!-- This is a speaker note --> |
| Background image |  |
| Fit image to slide |  |
| Two columns | Use the lead/split class, or HTML/CSS |
| Inline HTML | Allowed: html: true is set in .marprc |
See the Marp Markdown documentation for the full syntax.
- Use self-contained decks (
<name>/slides.md) for anything beyond a single screen of content. You get a clean home for the cover and deck-local assets. - Reference global assets with absolute paths (i.e.
/assets/images/logos/foo.webp), the build system rewrites these correctly for every output format. - Reference deck-local assets with
./assets/..., same: the build system relocates and rewrites them safely. - Keep one logical idea per slide: Marp is fast; abuse the separator.
- Commit early, diff often: That's the whole point of Markdown decks.
A "cover" is a single PNG, generated from the first slide of a Markdown file, suitable for thumbnails, social sharing, deck galleries, and READMEs.
For every deck slides/<path>/<deck>.md, the build system looks for a dedicated cover file in this order:
- Self-contained deck only: if the deck is named
slides.md, look forcover.mdin the same directory. - Any deck: look for
<deck>.cover.mdnext to the deck. - Fallback: use the deck file itself; its first slide becomes the cover.
| Deck path | Cover candidate (in order) |
|---|---|
slides/intro.md |
1. slides/intro.cover.md2. slides/intro.md |
slides/keynote/slides.md |
1. slides/keynote/cover.md2. slides/keynote/slides.cover.md3. slides/keynote/slides.md |
The cover format invokes Marp with --image=png plus the same --browser and --allow-local-files flags used for PDFs. Output filename matches the deck's derivation name (e.g., keynote.png), and the file lands in the final result/ directory alongside the HTML and PDF outputs.
This means you can write a dedicated, beautifully-styled title slide for sharing, distinct from your in-deck title, by simply creating a cover.md (or <deck>.cover.md) with a single slide.
All builds go through Nix. There is no Makefile by design. The flake exposes every deck and every format as a first-class package.
nix build '.#slides'The result/ symlink contains:
- One
*.htmlper deck - One
*.pdfper deck - One
*.pngcover per deck - A copy of
assets/(with global, twemoji, and any deck-specific assets rewritten correctly)
Every deck is exposed as a passthru attribute under slides, mirroring its path. A deck at slides/foo/bar.md (or slides/foo/bar/slides.md) is available as slides.foo.bar.{html,pdf,cover}.
# HTML only
nix build '.#slides.intro.html'
# PDF only
nix build '.#slides.intro.pdf'
# Cover PNG only
nix build '.#slides.intro.cover'
# Nested deck
nix build '.#slides.2026.q4-keynote.pdf'Inside nix develop:
# Browse every deck at http://localhost:8080
servenix fmt # Runs nixfmt + prettier per treefmt.nix
nix flake check # Verifies formattingNote
slides/**/*.md is excluded from Prettier (see .prettierignore) so Marp's specific directives and tokens (like step-by-step list items with * item) aren't reformatted.
The default theme custom-theme is declared in assets/css/theme.css (built on top of catppuccin-mocha.css and the Ubuntu fonts CSS).
To add your own:
-
Drop a CSS file in either
assets/css/orassets/themes/(both are registered as Marp theme paths in.marprc):themeSet: - assets/css - assets/themes
-
Start your theme file with a Marpit theme directive:
/*! @theme my-theme */ -
Reference it from any deck:
--- theme: my-theme ---
You can also override styles inline per deck using <style> tags or Marp's style: frontmatter key.
-
Drop the font files into
assets/fonts/. -
Create a dedicated CSS file in
assets/csswith a theme directive and@font-facerules. Reference fonts via absolute paths (i.e.url("/assets/fonts/MyFont-Regular.ttf")) so they resolve correctly in both HTML and PDF builds./*! @theme my-font */ .markdown-body, section { --fontStack-sansSerif: "My Font"; --fontStack-monospace: "My Font Mono"; font-family: "My Font", sans-serif; } @font-face { /* ... */ }
-
Use the font from your theme:
@import "my-font";
Because fonts are embedded into the repo and referenced through CSS that the build copies verbatim, PDFs ship with the typography baked in.
Project-wide Marp behavior lives in .marprc:
# Default locations to look for Marp themes
themeSet:
- assets/css
- assets/themes
# Allow raw HTML in Markdown (needed for advanced layouts)
html: trueThese are basic sensibles defaults, you can add any Marp CLI option here. The build system extends this file at build time to wire in Twemoji and the unsandboxed browser, but never overwrites your settings.
- Marp & Brave: both come from
nixpkgs, just editflake.lock's nixpkgs revision (nix flake update). - Twemoji: edit
versionandhashinpkgs/twemoji.nix. Runnix build .#twemojito verify.
See LICENSE.




