- Rust 72.7%
- HTML 19%
- JavaScript 6.4%
- Nix 1.9%
| crates | Fix date calc | |
| nix | Update flake | |
| static/js | Add meta tags, sanitise HTML | |
| templates | Simplfy recently planted section | |
| .gitignore | Forest glade theme: typography, search, and progressive enhancement | |
| Cargo.lock | Use pagefind directly; fix iframes | |
| Cargo.toml | Use pagefind directly; fix iframes | |
| flake.lock | Update flake | |
| flake.nix | Use pagefind directly; fix iframes | |
| README.md | Add vignette and responsive header refinements | |
Understory
A Rust static site generator purpose-built for Obsidian digital gardens. Wikilinks, backlinks, an interactive graph view, callouts, marginalia, and a handful of opinionated features for working-in-the- open garden workflows are first-class concerns.
Quick start
cargo run --bin understory -- build \
--content path/to/your/vault \
--output ./public \
--templates ./templates \
--static-dir ./static
...or use understory serve instead for a live-reloading dev server.
Configuration lives in understory.toml at the root of your content
directory. See the comments at the top of the sample file for the full
set of options.
Dialogs
Dialogs let you write group-chat-style exchanges between named recurring characters — useful for Socratic explanations (à la The Little Schemer), reading-group transcripts, or anywhere a conversation reads better than a monologue.
Writing a dialog
Use a fenced code block with the language tag dialog. Each turn is a
line that starts with the speaker's name followed by a colon:
```dialog
Sprig: Is `cons` a function?
Mossy: Yes — it builds a pair from two values.
Sprig: > [!note] Like a tuple constructor?
Mossy: Right. The first argument becomes the `car`, the second the
`cdr`. Continuation lines just indent under the speaker.
```
Rules:
- A line shaped like
Name: textstarts a new turn. Names accept letters, digits, spaces, hyphens, and underscores; they must start with a letter. - Indented or blank lines extend the current turn, so multi-paragraph speeches and lists work naturally.
- The body of each turn is regular markdown — bold, italics, inline code, links, wikilinks, embeds, even nested callouts all render as they would anywhere else on the page.
- Consecutive turns from the same speaker collapse the avatar and name, stacking the bubbles tightly under the first.
Defining the cast
Recurring characters live in understory.toml as [[characters]]
tables. Each entry has four fields:
[[characters]]
name = "Sprig"
avatar = "leaf" # bundled glyph, see below
color = "sage" # palette token, see below
side = "left" # "left" | "right"
| Field | What it does |
|---|---|
name |
Display label and Name: prefix matcher (case-insensitive). |
avatar |
Which bundled glyph to render in the avatar bubble. |
color |
Accent color for the avatar, name label, bubble tint, and border. |
side |
Which side of the chat the bubble sits on. iMessage-style: fixed per character. |
Speakers referenced in a dialog who aren't in the cast still render (with a fallback leaf avatar) and emit a build warning so you notice the typo.
Avatars
Eight bundled glyphs ship inside the binary as ×ばつ24 SVGs. Each is a small creature: a round body with vertical gradient and soft drop shadow, two eyes with white pupil highlights, a curved smile, and a distinguishing topper.
| Name | Looks like |
|---|---|
leaf |
Round body with two leaf-shaped "ears" sprouting up |
sprig |
Round body with a small twig and offshoot leaves on top |
mushroom |
Round body capped by a spotted mushroom dome |
moon |
Round body with a crescent moon adjacent, sleepy eyes |
sun |
Round body with rays radiating up, big bright smile |
flower |
Round body with five petals arrayed above |
feather |
Round body with feather-barb markings on top |
shell |
Round body topped by a spiral nautilus-style shell |
The body uses currentColor, so the avatar tints to each character's
accent color automatically. Faces use a stable dark warm-grey
(#1f1f23) that reads on any tint.
Color palette
Ten distinct hues are available, chosen for clear separation around the color wheel so 4–6 character casts stay visually distinguishable:
| Token | Hue family | Default hex |
|---|---|---|
sage |
green | #6f8a5a |
moss |
deep green | #4a7c59 |
teal |
green-blue | #3f8a82 |
ocean |
blue | #3a6ba8 |
lavender |
blue-purple | #7a8fce |
plum |
red-purple | #6b4a82 |
petal |
pink | #c97a8e |
rose |
red | #c25768 |
amber |
orange | #c97532 |
gold |
yellow | #b48a3a |
Each token expands to three CSS variables internally — a saturated avatar background, a softer bubble tint, and a sharper edge color — so the avatar carries the eye-catching identity cue while the bubble stays quiet enough for text to read cleanly on top.
Tip
For a 6+ character cast, pick colors that are roughly 60° apart on the wheel: e.g.
sage → ocean → plum → rose → amber → teal. Pairs sitting on the same side of the chat are the ones most likely to get visually confused, so spread their hues most aggressively.
Rendering
Each turn renders as <aside class="dialog-turn"> containing an
inline-SVG avatar and a meta column holding the speaker's name and
the bubble. The bubble's top corner on the speaker's side is squared
off (6px vs the standard 18px) so it reads like a tail pointing
back at the avatar. On screens narrower than 640px, the side-margins
collapse and bubbles use the full content width.
Example
Sprig: First time in Vancouver?
Mossy: Yes — the rain is gentler than I expected.
Lune: Don't forget your umbrella in October, though.
Sprig: Wait until you find the rainforest in Stanley Park.
Mossy: There's a *rainforest* inside a city park?
Helios: Temperate rainforest — different beast from the tropical kind.
...with this cast in understory.toml:
[[characters]]
name = "Sprig"
avatar = "leaf"
color = "sage"
side = "left"
[[characters]]
name = "Mossy"
avatar = "mushroom"
color = "plum"
side = "right"
[[characters]]
name = "Lune"
avatar = "moon"
color = "lavender"
side = "left"
[[characters]]
name = "Helios"
avatar = "sun"
color = "gold"
side = "right"
Markdown features
Understory parses with comrak and enables most GFM (GitHub-Flavored Markdown) extensions out of the box plus a handful of Obsidian-flavoured ones. The full list:
| Feature | Syntax | Notes |
|---|---|---|
| Wikilinks | [[Note Name]], `[[Note |
display]]` |
| Image embeds | ![[image.jpg]] |
Resolves bare names to anywhere in the vault, copies to output. |
| Note transclusions | ![[Other Note]] |
Renders as a styled link to the target page. |
| GFM tables | | A | B | etc. |
|
| Strikethrough | ~~deleted~~ |
|
| Autolinks | bare URLs and emails | |
| Task lists | - [x] / - [ ] |
|
| Footnotes | Body[^1] + [^1]: definition |
Promoted to marginalia in the right gutter on wide screens. |
| Description lists | term + : definition |
|
| Superscript | ^text^ |
|
| Frontmatter | YAML between --- delimiters |
|
| Emoji shortcodes | :smile: |
|
| Heading anchors | auto-generated IDs on every heading | Powers the table of contents. |
| Math (LaTeX) | $x + 1$, $$e = mc^2$$ |
See below. |
| Callouts / alerts | > [!NOTE] blockquotes |
See below. |
| Mermaid diagrams | ```mermaid code blocks |
Rendered client-side via CDN, only loaded on pages that use them. |
| Syntax highlighting | ```rust code blocks |
syntect with CSS-class output; light + dark themes. |
| Dialogs | ```dialog code blocks |
See the Dialogs section. |
Math
Inline math uses single-dollar delimiters; display math uses double:
The Pythagorean identity is $a^2 + b^2 = c^2$.
For energy-mass equivalence:
$$E = mc^2$$
Math is rendered client-side by KaTeX, loaded
on-demand from a CDN — only pages that contain at least one math
node link the KaTeX stylesheet and script. A MathDetect AST pass
sets a has_math flag during the build so the inclusion is
automatic.
Shared preamble
If you use the same \newcommand macros across many notes, define
them once in understory.toml:
[math]
preamble = """
\\newcommand{\\R}{\\mathbb{R}}
\\newcommand{\\N}{\\mathbb{N}}
\\newcommand{\\bra}[1]{\\langle #1 |}
\\newcommand{\\ket}[1]{| #1 \\rangle}
"""
The preamble is rendered once with globalGroup: true before any
inline math, registering the macros for every subsequent katex.render
call on the page.
Callouts
GitHub-flavoured alert blocks. Use a blockquote with [!KIND] on the
first line:
> [!NOTE]
> Useful information that users should know, even when skimming.
> [!TIP]
> Helpful advice for doing things better or more easily.
> [!IMPORTANT]
> Key information users need to know to achieve their goal.
> [!WARNING]
> Urgent info that needs immediate user attention to avoid problems.
> [!CAUTION]
> Advises about risks or negative outcomes of certain actions.
Each kind gets its own accent colour and unicode glyph in the title:
| Kind | Glyph | Accent |
|---|---|---|
NOTE |
i | ocean (blue) |
TIP |
✦ | sage (green) |
IMPORTANT |
★ | plum |
WARNING |
⚠ | amber |
CAUTION |
⊘ | rose (red) |
The body is regular markdown — wikilinks, code, lists, even nested alerts all work inside a callout.
Other features
Beyond what's covered above: backlinks and an interactive
force-directed graph, asset tree-shaking, OG image generation,
favicon auto-generation from the site title's first letter, a
lightweight client-side fuzzy search index, per-page popovers on
internal links, a collapsible file-explorer sidebar, and an
auto-refreshing dev server with debounced rebuilds. Per-feature
documentation is a work in progress; for now the source comments
in crates/understory_core/src/ are the canonical reference.
License
MIT OR Apache-2.0