Building¶
Reference for marimo-book's build pipeline, every CLI command, and
all opt-in features.
How a build works¶
marimo-book is a two-stage pipeline:
content/*.md + *.py + book.yml
↓ marimo-book preprocessor (validates config, walks TOC, renders pages)
↓
_site_src/
├── docs/
│ ├── intro.md ← copied from content/
│ ├── chapter1.md ← rendered from chapter1.py via `marimo export`
│ ├── stylesheets/extra.css
│ ├── javascripts/marimo_book.js
│ └── images/ ← assets copied verbatim
├── mkdocs.yml ← fully derived from book.yml
└── changelog.md ← optional, when include_changelog: true
↓ mkdocs build (Material theme + plugins)
↓
_site/ ← final static HTML, ready to deploy
The preprocessor never shells out to mkdocs; it just emits the
artifacts mkdocs can consume. This keeps the shell swappable —
zensical (Material's Rust successor) reuses
the same mkdocs.yml verbatim.
Build cache¶
marimo export ipynb re-executes every cell in a notebook from scratch
— the dominant cost for any book with non-trivial computation. To
avoid paying that cost on every rebuild, the preprocessor maintains a
content-addressed cache.
How it works
After each successful build, the preprocessor writes a manifest at
{book_root}/.marimo_book_cache/manifest.json recording, per .py
TOC entry: source mtime, source SHA-256, the staged output path, and
a timestamp. On the next build it consults the manifest:
| Check | If true | If false |
|---|---|---|
cache.version matches current schema |
continue | full miss (reset cache) |
marimo_book_version matches |
continue | full miss |
Hash of relevant book.yml fields matches |
continue | full miss |
| Per-entry: source mtime unchanged | HIT (skip render) | hash check |
| Per-entry: source hash unchanged | HIT (refresh mtime) | MISS — re-render |
book.yml fields that invalidate the cache: widget_defaults,
defaults, dependencies, launch_buttons, repo, branch, the
flattened toc. Fields that don't (palette, fonts, title, analytics)
only affect mkdocs.yml emission, which is always re-run.
Markdown TOC entries are not cached — Markdown render takes ~10 ms each, so the bookkeeping isn't worth it.
What the cache cannot detect (use --rebuild for these):
- Data files the notebook reads (
pd.read_csv("data/foo.csv")) env-mode dependency upgrades (pip install -U numpy)sandbox-mode PEP 723 dep changes inside the notebook (the notebook's own mtime catches some of these, but pinned versions on disk that change underneath you don't)- A
marimopackage upgrade (rare; bump the marimo-book version yourself or--rebuildif a notebook starts rendering wrong)
Inspecting the cache
Hand-edit at your peril; the next build will overwrite anything you
change. To reset: marimo-book clean (removes _site/, _site_src/,
and .marimo_book_cache/) or marimo-book build --rebuild (rebuilds
fresh + repopulates the cache in one step).
CLI commands¶
marimo-book new <directory>¶
Scaffold a fresh book.
Options
| Flag | Effect |
|---|---|
--force |
Write into an existing non-empty directory (may overwrite) |
The scaffold ships book.yml, content/intro.md, content/example.py,
a GitHub Pages workflow at .github/workflows/deploy.yml, a
.gitignore, and a starter README.
marimo-book build¶
One-shot static build. Emits _site/ ready to deploy.
marimo-book build # default: book.yml in cwd
marimo-book build -b docs/book.yml # explicit config path
marimo-book build -o public --strict # custom output, fail on warnings
marimo-book build --clean --sandbox # clean rebuild + force sandbox
Options
| Flag | Default | Effect |
|---|---|---|
-b, --book PATH |
book.yml |
Path to the book.yml config |
-o, --output PATH |
_site |
Output directory |
--strict |
off | Fail on warnings (broken in-tree links, missing files, etc.) |
--clean |
off | Remove _site/, _site_src/, and .marimo_book_cache/ before building (implies --rebuild) |
--rebuild |
off | Re-render every notebook regardless of cache state — use when data files or env-mode deps changed |
--sandbox / --no-sandbox |
follow book.yml |
Override the dependency mode |
Use --strict in CI. It surfaces issues that would otherwise be
silent on a successful build.
Use --clean when cutting a release. Otherwise stale files from
removed TOC entries linger in _site_src/ (intentional, for fast
incremental dev builds; not what you want in production).
marimo-book serve¶
Live-reload dev server.
marimo-book serve # http://127.0.0.1:8000/
marimo-book serve --port 9000 --no-watch
marimo-book serve --sandbox # slower iteration, but reproducible
Runs an initial build, then a watchdog observer re-runs the
preprocessor when files change under content/ or book.yml is
edited. mkdocs's livereload pushes the browser refresh.
Options
| Flag | Default | Effect |
|---|---|---|
-b, --book PATH |
book.yml |
Path to the book.yml config |
--host TEXT |
127.0.0.1 |
Dev-server bind address |
--port INTEGER |
8000 |
Dev-server port |
--no-watch |
off | Disable the source watcher (useful for debugging) |
--rebuild |
off | Cold-build the initial render (watcher rebuilds always honour the cache) |
--sandbox / --no-sandbox |
follow book.yml |
Override the dependency mode |
macOS browser-reload flake
On some macOS setups, mkdocs's browser auto-reload doesn't always fire. The build pipeline itself is reliable; if the browser doesn't refresh, hard-refresh (⌘-R).
marimo-book check¶
Validate book.yml and linked content without building. Fast — no
mkdocs invocation, no marimo export.
Catches: malformed book.yml, TOC entries pointing at missing files,
unsupported file types. Use as a pre-commit hook for fast feedback.
marimo-book clean¶
Remove build artifacts.
marimo-book clean # removes _site/, _site_src/, cache
marimo-book clean -o public # custom output dir
Optional features (opt-in via book.yml)¶
Each flag is opt-in (default off) and may require an extra:
| Flag | What it does | Extra |
|---|---|---|
social_cards: true |
Material's social plugin auto-generates per-page OpenGraph / Twitter card PNGs and injects the matching <meta> tags |
marimo-book[social] (also needs system libcairo2 libpango-1.0-0 libpangocairo-1.0-0) |
cross_references: true |
mkdocs-autorefs resolves [Heading text][] to whichever page contains that heading — the MkDocs analog of MyST {ref} |
marimo-book[autorefs] |
check_external_links: true |
htmlproofer HEAD-checks every <a href> and <img src> against the live web |
marimo-book[linkcheck] |
include_changelog: true |
Preprocessor copies CHANGELOG.md from the book root (or its parent) into the staged tree and appends a "Changelog" entry to the nav |
None |
pdf_export: true |
mkdocs-with-pdf renders the entire book through WeasyPrint into _site/pdf/book.pdf and adds a "Download PDF" link to the page footer |
marimo-book[pdf] (also needs the cairo + pango system libs above) |
social_cards — OpenGraph previews¶
Enable when you care about how the book looks when shared on social
media. Cards inherit your theme.palette.primary for the background
colour. ~5 s overhead per build for ~20 pages.
cross_references — autorefs¶
With cross_references: true, you can write:
…and [Anywidgets][] resolves to whichever page has # Anywidgets
as a heading. Lets you reorganise nav structure without breaking
inbound links.
check_external_links — htmlproofer¶
Slow (~1–3 s per outbound link). Keep off in CI for normal builds and
turn on only when cutting a release. Combined with
marimo-book build --strict, broken external links fail the build.
include_changelog — auto-publish CHANGELOG.md¶
Single source of truth: the CHANGELOG.md PyPI links to also becomes
a docs page. Looks first at book_dir/CHANGELOG.md, falls back to
book_dir.parent/CHANGELOG.md so the common docs/-subdir layout works
without extra config. Silent no-op when no CHANGELOG.md exists.
pdf_export — single-PDF download¶
Renders the entire book to one PDF. Adds a "Download PDF" link to the
footer of every page. Slow on large books (~30 s for ~50 pages); turn
off in serve and on in CI / for release builds.
The PDF inherits cover page metadata from your book.yml:
- Cover title ←
title - Cover subtitle ←
description - Author ←
authors[*].name - Copyright ←
copyright
Output lands at _site/pdf/book.pdf. Wire it into your deploy by
copying the whole _site/ directory as usual.
PDF builds in CI only
Local dev rarely needs the PDF. Set pdf_export: false in your
primary book.yml and override in CI:
…and tweak book.yml to read the env var. Or maintain a
book.ci.yml variant just for the deploy job.
Output layout¶
After marimo-book build:
your-book/
├── book.yml
├── content/ ← your sources (untouched)
├── _site_src/ ← preprocessor output (intermediate)
│ ├── docs/ ← staged Markdown + assets
│ └── mkdocs.yml ← generated from book.yml
└── _site/ ← final HTML (deploy this)
├── index.html
├── intro/index.html
├── chapter1/index.html
├── assets/ ← Material's CSS/JS
├── stylesheets/extra.css
├── search/ ← search index
├── sitemap.xml
└── pdf/book.pdf ← only if pdf_export: true
_site_src/ is intentionally preserved between builds — incremental
rebuilds in serve are much faster when the staged tree exists. To
force a wholly fresh build, use marimo-book build --clean.
Static reactivity¶
marimo-book can give static pages a real feel of interactivity for
marimo's discrete UI widgets — without a Python kernel at runtime. The
preprocessor re-executes the notebook once per widget value at build
time, ships the resulting cell outputs as a JSON lookup table, and a
small JS shim swaps the affected cells when the reader interacts with
the widget.
This is the kernel-free reactivity path for defaults.mode: static
(the only mode in v0.1.x). When v0.2 introduces WASM render mode,
WASM-rendered pages will get native reactivity via Pyodide and this
whole pipeline will be a no-op for them — the static fallback works
the same way either side of that transition.
Opt in via book.yml¶
precompute:
enabled: true # off by default
max_values_per_widget: 50 # graceful skip if a widget has more
max_combinations_per_page: 200 # graceful skip on multi-widget pages
max_seconds_per_page: 60 # wall-clock budget for one page
max_bytes_per_page: 10485760 # 10 MB inline-table budget
exclude_pages: [] # ["content/heavy_chapter.py"]
All caps default to safe values that work for typical exploratory
notebooks. Over-cap = render static, log a warning — the build
doesn't fail unless --strict is passed.
What's a precompute candidate?¶
The widget IS the annotation. Authors write normal marimo code; the
choice between discrete and continuous widgets implicitly declares
candidacy. Notebooks stay portable — zero imports from marimo-book
in the .py source.
| Widget call | Precompute? |
|---|---|
mo.ui.slider(steps=[0, 1, 5, 10]) |
✅ explicit value list |
mo.ui.slider(0, 10, step=1) |
✅ explicit step |
mo.ui.slider(0, 10) (no step) |
❌ continuous, render static |
mo.ui.dropdown(options=["a", "b"]) |
✅ |
mo.ui.dropdown(options={"a": 1, "b": 2}) |
✅ keys enumerated |
mo.ui.switch() / mo.ui.checkbox() |
✅ two values |
mo.ui.radio(options=[...]) |
✅ |
mo.ui.range_slider(...) |
❌ deferred to v2 |
Widget created via non-literal call (make_slider(low, high)) |
❌ value set not statically extractable |
Continuous sliders and any widget the AST scanner can't statically resolve fall back to the existing static render — no surprises, no silent failures.
What's NOT in v1¶
- Multi-widget pages. A page with two precomputable widgets falls back to static for both, with a warning. The cross-product semantics
- JS shim for joint widgets are deferred to v2.
- Path Y subgraph re-execution. v1 re-runs the whole notebook per value; v2 will use marimo's dataflow graph to re-execute only the affected subgraph (10–100× speedup for notebooks with expensive imports / data loads).
mo.ui.range_slider. Two-handle widget with pair-valued state needs more design.
Demo¶
The Authoring → Static reactivity demo page in this book shows the
feature live. View its source on GitHub to see exactly how the author
wrote it — there's nothing marimo-book-specific in the .py.
When to use it (and when not to)¶
Good fit: - Educational notebooks with "tweak this parameter and see what happens" - Discrete dropdowns of options with cheap-to-compute outputs - Single-slider plot explorations with ≤50 values
Bad fit: - Continuous sliders that need fine-grained interactivity (use WASM mode in v0.2 when it lands, or link to a molab session) - Notebooks where every cell is expensive (build time × N values) - Multi-widget cross-products with shared downstream cells (v2)
Build cost¶
- First export is the static fallback render — paid regardless.
- Each additional value = one full notebook re-execution. Use
max_seconds_per_pageto bound this; the orchestrator extrapolates after the first export and aborts if projected runtime exceeds. - The build cache (v0.1.0a4) interacts with precompute correctly:
toggling
precompute.enabledinvalidates affected pages.
ePub / other formats?¶
Not supported as a flag yet. Recipe via pandoc on the built site:
marimo-book build
pandoc _site/intro/index.html _site/chapter1/index.html ... \
-o book.epub \
--metadata title="My Book" \
--metadata author="Your Name"
If demand picks up, an [epub] extra is on the roadmap. File an
issue with your use case.
Build performance reference¶
Approximate timings on a modern laptop (M-series Mac, no sandbox mode):
| Operation | Time |
|---|---|
Parse book.yml, validate TOC |
<50 ms |
| Render one Markdown page | <10 ms |
marimo export for one notebook (no widgets) |
~500 ms |
marimo export for one notebook (heavy anywidgets) |
~2 s |
Full mkdocs build for ~20 pages |
~500 ms |
social_cards rendering |
~250 ms / page |
pdf_export rendering |
~600 ms / page |
check_external_links (per outbound URL) |
1–3 s |
sandbox mode adds ~5–10 s per notebook the first time (provisioning
a fresh uv env); subsequent runs hit the uv cache and are fast.
See Authoring → Dependencies for the full sandbox
walkthrough.