Changelog¶
All notable changes to marimo-book will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.1.1] — 2026-04-26¶
Patch release: two render-path bug fixes plus a working drawdata
anywidget demo on the docs site, and a top-level CNAME convention
so the docs site can ship under a custom domain.
Fixed¶
- Anywidget render under
text/markdownmime.marimo export ipynbsometimes downgrades an anywidget HTML bundle to atext/markdownblob with the<marimo-anywidget>tag fully HTML-escaped (<marimo-anywidget). The mime-bundle picker now detects that, unescapes, and routes through the existing rewriter so the static mount<div>is emitted. Without this, anywidgets authored via third-party libraries like drawdata would render as visible escaped HTML text instead of the live widget. - Precompute slider boot race. Material's
document$is an RxJSSubject(not aBehaviorSubject) — subscribers added after its initial emission miss it. Combined with thedefer'd shim script tag, direct page loads occasionally raced past the initialdocument$event, leaving the precompute control mount empty (and hidden by:empty { display: none }CSS, so invisible to authors). Boot is now belt-and-suspenders: run once viaDOMContentLoaded/ immediate, and subscribe todocument$for instant-nav swaps. All boot work is idempotent.
Added¶
- Top-level
CNAMEconvention. Drop aCNAMEfile at the book root next tobook.yml; the preprocessor copies it into the staged docs tree so mkdocs ships it as_site/CNAME. GitHub Pages preserves the custom-domain setting on every redeploy. - drawdata anywidget demo in the docs site
(
Authoring → Anywidget demo: drawdata) — a live click-to-draw scatter canvas that proves the static anywidget pipeline renders third-party widgets correctly.
Changed¶
marimobook.orgis the canonical docs URL (wasljchang.github.io/marimo-book/).
[0.1.0] — 2026-04-26¶
First stable release. marimo-book is suitable for real production
use. The book.yml schema is frozen within the 0.1.x series — fields
may be added (additive, backward-compatible) but no field will be
removed or have its meaning changed without a major version bump.
This release ships per-page WASM render mode via marimo's islands runtime, completing the render-mode story: every page can opt into its preferred reactivity model (static / static + precompute / WASM) based on the chapter's needs, with the rest staying fast and light.
Added — WASM render mode (per-page opt-in)¶
mode: wasmas a per-entry override inbook.ymlTOC entries. When set, the page is rendered through marimo'sMarimoIslandGeneratorinstead of our staticcells_to_markdownpipeline. Marimo's runtime + Pyodide load in the browser at first paint; cells become natively reactive, no precompute caps apply, continuous sliders work as you'd expect from a real notebook.book.defaults.modeis now widened toLiteral["static", "wasm"]for whole-book defaults;wasmper-entry override coexists.Bookconfig gainsFileEntry.effective_mode(default)helper that resolves the per-entry override against the book-wide default.- New module
src/marimo_book/transforms/wasm.pywithrender_wasm_page()— invokesMarimoIslandGenerator.from_file(), awaitsbuild(), returnsrender_head() + render_body(style="")ready to splice into the staged page. Per-page head injection (no globalextra_javascriptpollution). - Static reactivity (
precompute.enabled) is automatically a no-op for WASM pages — marimo's runtime handles reactivity natively, so there's no point in our build-time precompute pipeline. - New CSS in
assets/extra.cssoverriding marimo island fonts to Geist (matches our static theme), suppressing per-island margins, hiding marimo's loading spinner. Phase 1 styling — pixel-perfect match is a polish pass. - New live demo chapter
docs/content/wasm_demo.pyconfigured withmode: wasmin the docs site TOC. Demonstrates a continuousmo.ui.slider(1, 100)driving live Python computation in the browser — the same widget call would render as static-only on a non-WASM page (no precompute candidate without an explicit step).
Asset hosting¶
Marimo islands runtime + style are loaded from jsdelivr CDN by default
(matches what MarimoIslandGenerator.render_head() emits and what we
already do for Google Fonts + MathJax). Pyodide loads on demand from
its own CDN inside marimo's bundle. Self-hosting is on the roadmap
for the privacy-conscious / offline case but not yet implemented.
[0.1.0a6] — 2026-04-26¶
Multi-widget reactivity + dartbrains-driven fixes. Static reactivity
now handles independent and joint (cross-product) multi-widget pages.
Multi-line widget call definitions are recognised. The all-kwargs
slider style (mo.ui.slider(start=A, stop=B, step=N)) — marimo's
recommended form — now produces precompute candidates. Validated by
real testing on the dartbrains course site.
Added — joint multi-widget + multi-line widget calls¶
- Joint multi-widget precompute (cross-product). Widgets that share
downstream cells now precompute together via the cartesian product of
their values. Each joint group emits a single
<script class="marimo-book-precompute-group">metadata + lookup table block keyed byJSON.stringify([v1, v2, ...]). The JS shim reads every widget in the group on each input event, constructs the combo key, and swaps cells together. Bounded bymax_combinations_per_page— a 9-widget group with 5 values each is 1.95M combos and trips the cap; 2-widget groups with ~10 values each fit comfortably. - Connected-components grouping (
_group_widgets_by_downstream): union-find pass over the cell→widget map. Independent widgets stay in singleton groups (existing behaviour); widgets sharing any downstream cell get unioned into one joint group. - Multi-line widget call substitution. Widget calls spanning
multiple lines (the dartbrains pattern:
mo.ui.slider(\n start=0, \n stop=10,\n step=1,\n)) now substitute correctly. The splice replaces the entire(start_line, col)–(end_line, end_col)range with the unparsed call expression — multi-line call collapses to one line in the temp source consumed bymarimo export(never shown to the user).
Added — multi-widget independent precompute (1/2)¶
- Multi-widget independent precompute. Lifts the v0.1.0a5 single-widget-per-page restriction. Pages with N discrete widgets whose downstream cells are disjoint now precompute each widget independently. Each widget gets its own input control + lookup table, and the JS shim limits cell swaps to the widget that drives them. Joint widgets (sharing a downstream cell) still cause the whole page to render static with a clear "joint multi-widget precompute is deferred" warning.
- AST scanner now recognises
mo.ui.slider(start=A, stop=B, step=N)with all-kwargs form (marimo's recommended style and what dartbrains uses everywhere). Continues to recognise positional and start/stop-positional + step-kwarg forms. - New
estimate_renders_independent()helper sums per-widget renders (1 + sum(values_i - 1)) instead of the cartesian product. The preprocessor uses this againstmax_combinations_per_pageso the cap reflects realistic v1 cost — independent multi-widget pages no longer trip an astronomical cross-product number. - Substitution failures (e.g. multi-line widget call definitions)
surface as a clear
BuildReport.warningsentry instead of silently skipping. Authors get told which widget couldn't be precomputed and why.
Changed¶
precompute_page()signature: takescandidates: list[WidgetCandidate]instead of a single candidate. Single-widget callers pass a one-element list; behaviour matches v0.1.0a5 for the 1-widget case.- Per-widget script blocks (
<script class="marimo-book-precompute-widget">,<script class="marimo-book-precompute-table">) and control mounts (<div class="marimo-book-precompute-control">) now carry adata-precompute-widget="varname"attribute used to pair them on multi-widget pages. - Reactive cells gain a
data-precompute-widget="varname"attribute alongsidedata-precompute-cell="N"so the JS shim limits swaps to cells controlled by the changed widget.
Validated on dartbrains¶
Cache (v0.1.0a4) gives a 31× speedup on dartbrains warm rebuild (107s cold → 3.4s warm; 20 notebooks all cache-hit). Multi-widget independent works on simple cases; on dartbrains specifically, 4 of 4 widget-heavy chapters either trip the disjointness check (joint widgets, deferred to v2) or have value counts above the default cap. The unlock for dartbrains will be joint multi-widget cross-products (deferred), and multi-line widget call support for chapters like ICA.
[0.1.0a5] — 2026-04-25¶
Static reactivity for marimo's discrete UI widgets ships in two
PRs (#6 + #7). Authors enable precompute.enabled: true in book.yml
and discrete widgets (mo.ui.slider(steps=[...]),
mo.ui.dropdown(options=[...]), mo.ui.switch(),
mo.ui.checkbox(), mo.ui.radio(options=[...])) get real
client-side interactivity backed by build-time per-value rendering —
no Python kernel at runtime. Caps cap compute time and bundle size;
v1 supports single-widget pages.
Added — static reactivity execution (2/2)¶
- Per-value re-export pipeline: when
precompute.enabled: trueand a page has a single discrete-widget candidate, the preprocessor runsmarimo export ipynbonce per non-default value (substituting the widget'svalue=via AST surgery in a temp source), captures per-cell HTML, and stores the diff against the base render as a JSON lookup table embedded in the page. max_seconds_per_pagecap is now wall-clock-enforced: the first re-export's runtime is extrapolated; if projected total exceeds the budget, remaining values are skipped and the page renders static.max_bytes_per_pagecap is now byte-enforced after each combination is captured.- Reactive cells (those whose output differs across at least one value)
are wrapped in
<div class="marimo-book-precompute-cell" ...>for client-side targeting. Cells whose output is identical across all values are not stored — bundle stays bounded by what actually changes. - Client-side JS shim (
assets/marimo_book.js): renders the input control (range slider, select, or checkbox depending on widget kind), reads the embedded lookup table, and swaps reactive cell HTML on input — smooth, no page reflow, headers/sidebar/scroll position all preserved. - New CSS for
.marimo-book-precompute-controland the input controls (Material-themed, accent-coloured slider, tabular-numerics for the value label). - New "Static reactivity" section in
docs/content/building.mddocumenting the feature, the detection rules, the caps, and the v1 limitations (single-widget pages only, Path-X execution path). - New live demo chapter at
docs/content/precompute_demo.py— temperature-conversion slider that swaps a Markdown table per value. Visible atAuthoring → Static reactivity demoon the docs site withprecompute.enabled: true. Preprocessing OKsummary now reports precompute counts:(14 pages, 13 rendered, 0 cached, 1 precomputed, 0 skipped).- v1 limitation: multi-widget pages render every widget static with a warning. Multi-widget cross-products + Path-Y subgraph re-execution are deferred to v2 / when WASM render mode lands.
Added — static reactivity foundation (1/2)¶
book.ymlprecomputeblock (off by default) — opt-in static reactivity for discrete marimo UI widgets. Five fields:enabled,max_values_per_widget(default 50),max_combinations_per_page(200),max_seconds_per_page(60),max_bytes_per_page(10 MB),exclude_pages([]).- AST scanner (
src/marimo_book/transforms/precompute.py) that finds precompute candidates without executing the notebook. Recognises:mo.ui.slider(steps=[...]),mo.ui.slider(start, stop, step=N)(or 3 positional args),mo.ui.dropdown(options=[...]),mo.ui.dropdown(options={...}),mo.ui.switch(),mo.ui.checkbox(),mo.ui.radio(options=[...]). Continuous sliders without an explicit step are deliberately skipped (render static). Non-literal arguments (mo.ui.dropdown(options=opts)) are skipped — value sets must be statically extractable. - Preprocessor preview pass: when
precompute.enabled: true, every.pypage is scanned, count caps are applied, and over-cap widgets are recorded asBuildReport.warnings("rendered static") with the page + widget name + which cap was hit.BuildReportgainswidgets_precomputed/widgets_skippedcounters. - Build cache
_book_signaturenow includes theprecomputeblock, so toggling the flag invalidates rendered notebooks correctly.
This is the foundation only — the actual per-value re-export pipeline
+ client-side JS swap shim ship in the next PR. Until that lands,
widgets_precomputed is a would-precompute count: useful for tuning
caps before paying for execution.
[0.1.0a4] — 2026-04-25¶
Build-cache + PDF + docs release. Cuts repeat-build time on books with non-trivial notebooks (the dartbrains use case) from minutes to seconds.
Added¶
- Incremental build cache. The preprocessor now caches
marimo export ipynboutputs at{book_root}/.marimo_book_cache/manifest.jsonkeyed by source content hash, marimo-book version, and relevantbook.ymlfields (widget_defaults,defaults,dependencies,launch_buttons,repo,branch,toc). Subsequent builds skip notebooks whose source hasn't changed. Typical edit-one-chapter rebuild on a 20-notebook book drops from "every notebook" to "only the edited one". Markdown entries are not cached (10 ms each, not worth the bookkeeping). marimo-book build --rebuildandmarimo-book serve --rebuild— bypass the cache for the current invocation. Use when you changed something the cache can't detect (data file the notebook reads, env-mode dep upgrade).--cleancontinues to wipe_site_src/and now also wipes.marimo_book_cache/, which has the same effect.- Build summary line now reports cache stats:
Preprocessing OK (13 pages, 11 rendered, 1 cached at _site_src). book.ymlpdf_export: boolflag — when true, emits themkdocs-with-pdfplugin so the build produces a single_site/pdf/book.pdfrendered through WeasyPrint, with a "Download PDF" link injected into the footer. Cover metadata (title, subtitle, author, copyright) inherits from existingbook.ymlfields. Requires the newmarimo-book[pdf]extra; needs the samelibcairo2/libpangosystem deps associal_cards.- New "Building" page in the docs (
content/building.md) — full reference covering the two-stage build pipeline, every CLI command with all flags, the five opt-in feature flags with their extras, the_site_src/and_site/output layouts, an ePub recipe via pandoc, and approximate build-performance numbers. Now also covers the build cache + invalidation rules.
[0.1.0a3] — 2026-04-25¶
Visual + authoring overhaul plus a release-flow modernisation.
Added¶
book.ymlcross_references: boolflag opts into themkdocs-autorefsplugin so authors can write[Heading text][]and have it resolve to whatever page has that heading — the MkDocs analog of MyST{ref}. Requires the newmarimo-book[autorefs]extra.book.ymlinclude_changelog: boolflag — when true, the preprocessor copiesCHANGELOG.mdfrom the book root into the staged docs tree and appends a "Changelog" entry to the nav. Single source of truth: the same file PyPI links to also becomes a docs page.- Default stylesheet (
assets/extra.css) modernized: zinc neutrals, near-black dark scheme (#0a0a0a), Geist Sans + Geist Mono viatheme.font, indigo accent on h1 / header title, uppercase tracked section labels in left sidebar + right TOC, hairline footer that matches page bg in both schemes. Carries forward to zensical. - README badges (PyPI version, Python versions, CI, License, Docs).
- Live admonition / math / table / code examples on the Authoring page.
- Cross-references documentation on the Authoring page (page-to-page, anchors, abbreviations, snippets, autorefs).
release-drafterworkflow + config — every merged PR updates a draft GitHub Release; tagging publishes it. Categorises by PR label (Added / Changed / Fixed / Removed / Documentation / Build & CI).
Changed¶
- Versioning: switched to
hatch-vcsfor dynamic versions derived from git tags.pyproject.tomlno longer carries a hard-codedversion; taggingv0.1.0a3is sufficient to publish a wheel versioned0.1.0a3.src/marimo_book/__init__.pyreads__version__viaimportlib.metadata. Eliminates the version-skew bug class (3 files no longer need to stay in sync per release). PUBLISHING.mdrewritten around the new flow: tag-driven release, optional CHANGELOG-date PR, release-drafter populating release notes.
Removed¶
- Breaking: MyST migration transforms (
{download}role rewrite,:::{glossary}fence stripping). marimo-book now uses Material's Markdown dialect exclusively. Books written for marimo-book have always used Material syntax (!!! noteadmonitions,[label](page.md)links); the removed transforms only affected content ported from Jupyter Book. To migrate: replace{download}\text` withtextand remove:::{glossary}/:::fence markers (the inner definition lists pass through Material'sdef_list` natively).
[0.1.0a2] — 2026-04-24¶
Metadata + ergonomics pass on top of 0.1.0a1.
Added¶
book.ymlgains two fields:url— canonical public URL, emitted as mkdocssite_urlso the social plugin and sitemap.xml get fully-qualified paths.social_cards: bool— opts into Material'ssocialplugin for auto-generated OpenGraph / Twitter preview images per page. Requires the newmarimo-book[social]extra (pip install 'marimo-book[social]') which pullsmkdocs-material[imaging]+ Pillow + cairosvg.pyproject.tomlDocumentationproject URL now links directly to the docs site, so PyPI's sidebar gains a "Documentation" link in addition to Repository / Issues / Changelog.
Changed¶
- CI + docs deploy workflows install
libcairo2/libpangoon Ubuntu so the social plugin's SVG→PNG rendering works.
Fixed¶
info.licensestill reportsNoneon pypi.org JSON API (this is a Warehouse-side transition fromLicense→License-Expressionper PEP 639). The real PyPI page and the wheel METADATA both report MIT correctly.
[0.1.0a1] — 2026-04-24¶
First alpha release. Usable end-to-end for single-book sites.
Added¶
CLI (marimo-book ...)
new <dir>— scaffold a new book (book.yml, content/, .gitignore, .github/workflows/deploy.yml, README).--forceto write into non-empty directories.build— preprocess + emit static site to_site/.--strictfails on warnings;--cleanblows away prior build artifacts first.serve— dev server with live reload. Runs an initial build, spawnsmkdocs serve, and a watchdog observer rebuilds on changes tocontent/orbook.yml.check— validatebook.yml+ referenced files without building.clean— remove_site/,_site_src/,.marimo_book_cache/.
Config (book.yml)
- Pydantic v2 schema with readable errors for unknown keys.
- Discriminated-union TOC (
file:/url:/section:+children:). - Author metadata, branding (logo, favicon, palette, fonts), launch buttons, bibliography paths, analytics (Plausible / Google), per-page render defaults, per-widget-class default state.
- Top-level
shell:reserved for futurezensical/jinjatargets.
Preprocessor transforms
marimo_export—.py→ Markdown + inline HTML viamarimo export ipynb --include-outputs. Handles hide_code, mime bundles (text/html, text/markdown, image/png|jpeg|svg+xml, text/plain, streams, errors), and first-setup-cell elision.callouts—<marimo-callout-output>→ Material admonition with the kind mapped (info/note/success/tip/warning/danger/failure/neutral).anywidgets—<marimo-anywidget>→<div class="marimo-book-anywidget">mount; AST-walks cell source to extract literal widget kwargs; merges withbook.ymlwidget_defaults. Strips<marimo-ui-element>/<marimo-slider>/ etc. wrappers around kernel-dependent controls that have no static analog.md_roles—{download}\label` →label;:::{glossary}` fence stripping.link_rewrites—.ipynbcross-refs →.md(when target exists);../images/→images/in both Markdown links and HTML attrs.
Shell generator
book.yml→mkdocs.ymlwith Material theme, standard pymdownx extensions (arithmatex, admonition, blocks, details, highlight, superfences, tabbed, tasklist), sensible nav feature flags, and anextra.cssderived from the book's palette.
Runtime shim (assets/marimo_book.js)
- Minimal anywidget-compatible model loader. At page load, finds every
.marimo-book-anywidgetmount, decodes the inlined ES module fromdata-js-url, and callsmodule.default.render({model, el})with a seeded model. Works with Material's instant-navigation viadocument$.subscribe.
Verified¶
- Full test suite: 46 passing across config, transforms, CLI, and watcher.
- End-to-end dartbrains build (34 TOC entries, 20 marimo notebooks): 0 preprocessor errors, 2 cosmetic mkdocs warnings (both broken-link issues in dartbrains source content, not tool bugs).
- Widget-heavy chapter (MR_Physics.py, 9 anywidgets) renders and animates in a browser without marimo's frontend runtime.
Known limitations¶
- No WASM / hybrid render modes yet (static only).
- No dependency-graph-aware incremental cache; every rebuild is a full rebuild.
- MyST cross-refs with
{ref},{numref},{eq},{cite}, etc. are stripped through unchanged (no usage in any book we've migrated yet). - Material for MkDocs is entering maintenance mode in ~12 months;
marimo-bookis explicitly designed to port to zensical when it stabilises (samemkdocs.yml, different build command).