2
3
Fork
You've already forked understory
0
Digital Garden SSG
  • Rust 72.7%
  • HTML 19%
  • JavaScript 6.4%
  • Nix 1.9%
2026年05月18日 15:16:58 -07:00
crates Fix date calc 2026年05月18日 15:16:58 -07:00
nix Update flake 2026年05月17日 11:31:28 -07:00
static/js Add meta tags, sanitise HTML 2026年05月16日 20:54:11 -07:00
templates Simplfy recently planted section 2026年05月18日 14:29:43 -07:00
.gitignore Forest glade theme: typography, search, and progressive enhancement 2026年05月16日 20:53:03 -07:00
Cargo.lock Use pagefind directly; fix iframes 2026年05月17日 15:08:40 -07:00
Cargo.toml Use pagefind directly; fix iframes 2026年05月17日 15:08:40 -07:00
flake.lock Update flake 2026年05月17日 11:31:28 -07:00
flake.nix Use pagefind directly; fix iframes 2026年05月17日 15:08:40 -07:00
README.md Add vignette and responsive header refinements 2026年05月16日 20:53:24 -07:00

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: text starts 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