(+ 1 2 3)6May 1, 2026
This is the first Babashka-authored post on Clojure Civitas — and at the same time, a small walkthrough of how to author your own. The page is rendered with Babqua, a Quarto extension that evaluates {.clojure .bb} code blocks with Babashka during the render and replaces each block with its evaluated output. Kindly metadata controls how each value renders, the same convention Clay uses for Clojure documents.
Babqua is a sibling of Janqua, which does the same for Jank. The two extensions deliberately share authoring conventions — fence syntax, Kindly kinds, frontmatter shape — so a post can move between languages without rewriting its scaffolding.
A huge thanks to Michiel Borkent and the wider Babashka community for the runtime that makes this possible.
Babashka is a Clojure interpreter aimed at scripting and shell tasks. It starts in milliseconds, runs as a single binary, and includes a wide set of built-in libraries — JSON, Hiccup, HTTP, filesystem, process, and more. The pods ecosystem extends it further (SQLite, AWS, HTML parsing, …) without changing the deployment story.
For a notebook-style post, the appeal is reproducibility: a .qmd file plus a bb.edn (when it needs extra deps) is enough to render anywhere bb is installed. No JVM warm-up, no project skeleton.
Five small demonstrations, working up from a primitive expression to a recursive SVG fractal. Each one fits in a single block and exercises a different corner of Babashka’s standard library or Babqua’s rendering.
Code blocks without Kindly metadata render as code output. The simplest possible block:
Annotate a value with ^:kind/... to tell Babqua how to render it:
The block below uses babashka.process/shell to invoke git log with a custom format string, splits the output line-by-line, and turns each line into a map. The result is a vector of recent commits, ready to chart:
State carries across blocks the way it does in any REPL session — commits defined above is visible below.
Aggregating into a per-month bar chart with Vega-Lite is one expression. Babqua serializes the value to JSON and Vega renders it client-side:
The next block uses babashka.fs to enumerate the top-level directories under src/ and count Clojure source files (.qmd, .clj, .cljc, .cljs) in each. Anchoring on the repo root via git rev-parse keeps the demo working from any working directory inside the checkout:
(require '[babashka.fs :as fs])
(def repo-root
(-> (process/shell {:out :string} "git" "rev-parse" "--show-toplevel")
:out str/trim))
(defn count-sources [dir]
(->> ["qmd" "clj" "cljc" "cljs"]
(mapcat #(fs/glob dir (str "**/*." %)))
count))
(def top-categories
(->> (fs/list-dir (fs/path repo-root "src"))
(filter fs/directory?)
(map (fn [dir] {:name (str (fs/file-name dir))
:count (count-sources dir)}))
(filter (comp pos? :count))
(sort-by (juxt (comp - :count) :name))
(take 8)))
top-categoriesRendering the result as a Mermaid diagram is a string template away. Kindly’s :kind/mermaid takes a vector-wrapped string:
^:kind/mermaid
[(let [;; prefix every node ID to avoid colliding with Mermaid keywords
;; (a category called "graph" would otherwise clash with `graph LR`)
node-id (fn [category-name]
(str "cat_" (str/replace category-name #"[^A-Za-z0-9_]" "_")))
edges (for [{:keys [name count]} top-categories]
(str " root --> " (node-id name)
"[\"" name "/ (" count ")\"]"))]
(str "graph LR\n root[\"src/\"]\n" (str/join "\n" edges)))]Hiccup happens to handle inline SVG just as readily as HTML — the tag is a keyword, the attributes are a map, and the children are nested vectors. Recursion gives us a Sierpinski triangle in a few lines:
(def sin60 (/ (Math/sqrt 3) 2))
(defn sierpinski [x y size depth]
(if (zero? depth)
[[:polygon
{:points (str x "," y " "
(+ x size) "," y " "
(+ x (/ size 2)) "," (- y (* size sin60)))}]]
(let [half (/ size 2)]
(mapcat (fn [[x y]] (sierpinski x y half (dec depth)))
[[x y]
[(+ x half) y]
[(+ x (/ half 2)) (- y (* half sin60))]]))))
^:kind/hiccup
[:svg {:xmlns "http://www.w3.org/2000/svg"
:viewBox "0 0 256 224"
:width 560
:fill "#d7161b"
:style "display:block;margin:0 auto"}
(sierpinski 0.0 222.0 256.0 7)]Hiccup-as-SVG is not limited to primitive shapes. The block below builds a tree by recursion, but the leaves are <image> elements pointing at the same babashka-logo.png we embedded above. Each generation produces two children at ±32° from the parent’s heading (with a small random jitter), anchored at the parent’s two top corners so they spread outward like a Pythagoras tree. Five levels deep gives 63 logos. A seeded java.util.Random keeps the layout identical on every render.
(def ^java.util.Random random-source (java.util.Random. 42))
(defn jitter
"A random number drawn uniformly from [-radius, radius]."
[radius]
(- (* 2 radius (.nextDouble random-source)) radius))
(defn logo-tree
"Returns a flat seq of {:x :y :angle :scale} placements. Each call
yields one logo (centered above (anchor-x, anchor-y) and rotated by
`angle`), then recursively yields two children at the parent's
top-left and top-right corners, rotated ±32° from the parent's heading."
[anchor-x anchor-y angle scale depth]
(let [radians (Math/toRadians angle)
sine (Math/sin radians)
cosine (Math/cos radians)
half-height (* 100 scale) ;; logo is 200×200
center-x (+ anchor-x (* half-height sine))
center-y (- anchor-y (* half-height cosine))
tip-x (+ anchor-x (* 2 half-height sine))
tip-y (- anchor-y (* 2 half-height cosine))
side-x (* half-height cosine) ;; tip → top-corner
side-y (* half-height sine)
child-scale (* scale (+ 0.55 (jitter 0.05)))]
(cons {:x center-x :y center-y :angle angle :scale scale}
(when (pos? depth)
(concat
(logo-tree (- tip-x side-x) (- tip-y side-y)
(+ angle -32 (jitter 5)) child-scale (dec depth))
(logo-tree (+ tip-x side-x) (+ tip-y side-y)
(+ angle 32 (jitter 5)) child-scale (dec depth)))))))
(defn logo-svg
"Wrap one logo placement in an SVG `<g>` that translates, rotates, and
scales a `<image>` reference to the shared logo file."
[{:keys [x y angle scale]}]
[:g {:transform (str "translate(" x " " y ") rotate(" angle ") scale(" scale ")")}
[:image {:href "babashka-logo.png" :x -100 :y -100 :width 200 :height 200}]])
^:kind/hiccup
[:svg {:xmlns "http://www.w3.org/2000/svg"
:viewBox "-280 50 560 360"
:width 560
:style "display:block;margin:0 auto"}
(map logo-svg (logo-tree 0 380 0 0.6 5))]The path from “I want to write a Babashka post” to “PR open”:
Beyond the standard Civitas setup (Quarto, Java 21, Clojure CLI), Babashka posts need only one extra tool:
bb) — see the install instructions for your system.If your post needs extra Clojure libraries or pods, drop a bb.edn next to the post (or at the project root) — Babashka picks it up automatically.
If you only contribute Clojure posts and skip Babashka, you can still build the site — Babashka pages will surface in-page error blocks where evaluated output should be, and the rest of the site renders normally.
Drop a .qmd file at src/<topic>/<post>.qmd with a Babqua-aware frontmatter:
Then write the body. {.clojure .bb} fenced blocks are evaluated; .clojure enables syntax highlighting, .bb triggers evaluation. Use Kindly metadata (^:kind/hiccup, ^:kind/vega-lite, ^:kind/plotly, ^:kind/mermaid, ^:kind/table, …) to control the rendering of each value. The Babqua docs list the full set of supported kinds and per-block options.
This works because the repo is already set up for it: a _extensions symlink at the root pointing to site/_extensions/ (where the Babqua extension lives), and a minimal _quarto.yml that lets Quarto find the filter without dragging in the full-website context. Symlinks recreate automatically on Linux/macOS; on Windows you may need git config --global core.symlinks true plus Developer Mode.
A live-reload server starts; saves trigger a re-render. Each render spawns a fresh bb process by default — fast enough for most posts. If your post does expensive setup (loads a pod, fetches data), you can opt into a persistent Babashka session that keeps state warm across saves:
# Start once per session, before `quarto preview`
bb site/_extensions/scicloj/bb/babqua-lifecycle.bb start
# Run preview as normal — the filter detects the live REPL and routes
# evaluations through it.
quarto preview src/<topic>/<post>.qmd
# When you're done, stop it.
bb site/_extensions/scicloj/bb/babqua-lifecycle.bb stopWhile a persistent session is running, defs and requires carry across renders the way they would in any Clojure REPL. The session creates three lifecycle dotfiles at the repo root (.babqua-pid, .babqua-nrepl-port, .babqua-bb.log) — they’re gitignored. See the Workflow chapter of the Babqua docs for the full set of lifecycle controls.
If you want to see the post inside the actual site — theme, listings, navigation — you can render it in full-site context:
This runs Clay and then quarto preview site on the whole website. It can be quite slow on some machines (the site has many pages); for routine iteration, step 3’s single-file preview is much faster.
CI already has Babashka installed (Janqua needed it for clj-nrepl-eval, and Babqua reuses the same binary), so no extra workflow changes are needed. draft: true keeps the post out of the public posts listing while you iterate; drop the line when you’re ready to publish.
This post was the first Babashka-authored page on Civitas. We’d love to see more. Babashka is well suited to small, scriptable demonstrations — fetching data, walking a directory, calling a pod, parsing a log — and posts that exercise one of those corners are exactly the kind of thing that makes the language and its ecosystem more discoverable.
If you hit something that doesn’t work, please open an issue on Babqua or on Civitas. Feedback shapes both.