options
2 posts

Reference for every config key, taxonomy, design token, sidebar mode, and JavaScript hook in basalt.

Configure basalt almost entirely through config.toml. Every key below is optional unless marked required; pages that need no customisation beyond front-matter work out of the box.

Site config

The canonical schema lives in config.toml in the theme repo. The table below is the prose companion. Each key sits under [extra].

Primary keys

KeyTypeRequiredDefaultNotes
authorstringyesJSON-LD Person markup, RSS author fields.
author_displaystringyesShown in the sidebar footer.
tagstringyesSite title rendered in monospace in the sidebar header.
sidebar_subtitlestringnoSmall muted line below the title.
biostringnoOne or two sentences shown in the sidebar on desktop.
default_theme"light" | "dark"no"light"Applied before first paint by theme.js to avoid FOUC.
prompt_glyphstringno"❯"Shown next to the active sidebar nav item.
comments"off" | "mastodon" | "bluesky"no"off"Default comment platform for all posts; pages can override in their own front-matter.
asciinema_themestringno"asciinema"Asciinema player skin (e.g. "monokai").
bg_default_positionstringno"center"Default CSS background-position for page backgrounds.
palette_actionsarrayno[]Declarative command-palette entries — see JavaScript hooks.

[[extra.sidebar_nav]] is an array of objects. Defining it activates static sidebar mode; omitting it activates filesystem mode. See Sidebar for the full model.

[[extra.sidebar_nav]]
id    = "home"
label = "home"
href  = "/"

[[extra.sidebar_nav]]
id    = "blog"
label = "blog"
href  = "/blog/"

[[extra.sidebar_nav]]
id       = "docs"
label    = "docs"
href     = "/docs/"
children = [
  { id = "install", label = "install", href = "/docs/install/" },
  { id = "config",  label = "config",  href = "/docs/config/"  },
]

All keys under [extra.social] are optional. Only keys that are set render in the social row. The value is your handle or username, and the template constructs the full URL. Two exceptions: mastodon (full URL required, so rel="me" can be set) and email (pass a mailto: URI or full URL).

Developer platforms

KeyValueURL pattern
githubusernamegithub.com/<username>
gitlabusernamegitlab.com/<username>
codebergusernamecodeberg.org/<username>
stackoverflowuser IDstackoverflow.com/users/<id>
hackernewsusernamenews.ycombinator.com/user?id=<username>
devtousernamedev.to/<username>
keybaseusernamekeybase.io/<username>

Social

KeyValueURL pattern
twitterhandletwitter.com/<handle>
blueskyhandlebsky.app/profile/<handle>
mastodonfull URLused as-is (sets rel="me")
threadsusernamethreads.net/@<username>
facebookusernamefacebook.com/<username>
instagramusernameinstagram.com/<username>
tiktokusernametiktok.com/@<username>
snapchatusernamesnapchat.com/add/<username>
pinterestusernamepinterest.com/<username>
linkedinusernamelinkedin.com/in/<username>

Video & streaming

KeyValueURL pattern
youtubechannel handleyoutube.com/@<handle>
twitchusernametwitch.tv/<username>

Messaging

KeyValueURL pattern
discordinvite codediscord.gg/<code>
telegramusernamet.me/<username>
whatsappphone numberwa.me/<number>
signalphone numbersignal.me/#p/<number>
matrixuser:servermatrix.to/#/@<user:server>

Music & media

KeyValueURL pattern
spotifyuser IDopen.spotify.com/user/<id>
bandcampsubdomain<subdomain>.bandcamp.com
soundcloudusernamesoundcloud.com/<username>
letterboxdusernameletterboxd.com/<username>
goodreadsuser IDgoodreads.com/user/show/<id>

Gaming

KeyValueURL pattern
steamcustom URL namesteamcommunity.com/id/<name>
itchiousername<username>.itch.io
redditusernamereddit.com/user/<username>

Support & funding

KeyValueURL pattern
kofiusernameko-fi.com/<username>
patreonusernamepatreon.com/<username>
buymeacoffeeusernamebuymeacoffee.com/<username>

Contact

KeyValueNotes
emailmailto:you@example.comFull URI; rendered as @ link.
rssfeed URLShown separately from the social row in the sidebar footer and mobile meta.

Taxonomies

basalt ships five pre-declared taxonomies. You can omit any from your own config, or add your own.

[[taxonomies]]
name = "tags"
feed = true   # generates /tags/atom.xml and /tags/rss.xml

[[taxonomies]]
name = "kind"

[[taxonomies]]
name = "status"

[[taxonomies]]
name = "audience"

[[taxonomies]]
name = "shape"

Zola generates a term-list page at /<taxonomy>/ and individual term pages at /<taxonomy>/<term>/. basalt renders these via taxonomy_list.html and taxonomy_single.html. Each term page lists all pages carrying that term.

Blog post taxonomies

Blog posts typically use only tags:

[taxonomies]
tags = ["zola", "rust", "tooling"]

Project taxonomies

Project pages use the full set:

[taxonomies]
tags     = ["zola", "theme"]
kind     = ["weekend"]       # weekend / production / wip / archived
status   = ["alive"]         # alive / paused / archived / stable
audience = ["solo"]          # solo / team / public / clients
shape    = ["theme", "cli"]  # free-form; joined with " / " in panes

Taxonomy chips on project pages are links. Clicking a chip goes to the term page listing all projects with that classification.

The partials/tag.html partial renders a chip as a <a> when taxonomy is set, or a <span> otherwise (used for GitHub topics on project cards, which aren't site taxonomies).


Design tokens

All colour values are CSS custom properties declared in sass/_tokens.scss. Nothing else in the codebase hard-codes a hex value; every component references these vars.

Colour tokens

TokenLightDarkUsage
--bg#f8f9fa#18181bPage and sidebar background
--bg-rgb248, 249, 25024, 24, 27RGB components of --bg. Used by background overlay calculation; keep in sync.
--surface#ffffff#1f1f23Cards, panes, code blocks
--text#1a1a1a#f4f4f5Primary prose text
--muted#6b7280#a1a1aaSecondary / meta text
--accent#3b82f6#60a5faLinks, active states, borders
--term#10b981#34d399Terminal green — active nav glyph, online indicators
--border#e5e7eb#3f3f46Input and element borders
--soft-border#eceef1#2a2a2fSubtle dividers
--code-bg#f3f4f6#27272aInline code and hover backgrounds
--tag-bg#eef2ff#1e293bTag / chip backgrounds
--tag-text#4338ca#93c5fdTag / chip text

Heat ramp tokens

Used for freshness or activity indicators. Green = fresh/low, amber = mid, red = hot/stale.

TokenLightDark
--heat-low#10b981#34d399
--heat-med#ca8a04#facc15
--heat-high#dc2626#f87171

Overlay and shadow tokens

TokenNotes
--overlaySemi-transparent dark veil for dialog backdrops
--shadow-lgLarge drop shadow
--shadow-mdMedium drop shadow (e.g. sidebar)
--scrollbarCustom scrollbar thumb colour

Asciinema / terminal chrome tokens

--term-chrome-bg, --term-chrome-bar, --term-chrome-border, --term-chrome-text, --term-chrome-btn colour the asciinema player chrome. Light defaults track the one-light player skin; dark defaults track night-owl.

Reskinning

Override any token in your own SCSS without forking the theme:

// your-site/sass/_overrides.scss
:root {
  --accent: #c9485b;
  --term:   #c9485b;
}
[data-theme="dark"] {
  --accent: #f87171;
  --term:   #f87171;
}

Import your override file after basalt's token import in your entry SCSS.


The sidebar nav has two modes, selected once per site by the presence or absence of config.extra.sidebar_nav. Both modes emit the same CSS classes (.sidebar-nav-item, .sidebar-nav-children), so styling lives entirely in sass/_sidebar.scss.

There is no per-section mode override, no tree-mode flag, and no collapsible <details> expansion — children are always visible.

Static mode

Activated by: defining [[extra.sidebar_nav]] in config.toml.

The sidebar renders the array in order. Each entry:

FieldRequiredNotes
idyesUnique identifier (not rendered; used for active-state matching).
labelyesDisplay text.
hrefyesDestination URL.
childrennoArray of { id, label, href } — depth cap 2.

The active item is highlighted by comparing current_path against href. An item whose href is a prefix of current_path is also highlighted, which is useful for section roots.

[[extra.sidebar_nav]]
id    = "home"
label = "home"
href  = "/"

[[extra.sidebar_nav]]
id    = "options"
label = "options"
href  = "/options/"

[[extra.sidebar_nav]]
id       = "blog"
label    = "blog"
href     = "/blog/"
children = [
  { id = "tags", label = "tags", href = "/tags/" },
]

Filesystem mode

Activated by: omitting extra.sidebar_nav entirely.

The theme walks the Zola section graph from the root (_index.md), rendering sections and their pages up to three levels deep (root → sub → sub-sub). A "home" entry pointing to / is auto-prepended.

Depth cap is 3 because Tera macros do not survive {% include %} boundaries; the walk is unrolled statically in the template.

Hiding content: set extra.hide_from_sidebar = true in a page or section's front-matter. For sections, omission cascades: the section and all its descendants disappear from the nav.

# content/private/_index.md
[extra]
hide_from_sidebar = true   # hides /private/ and everything under it

Individual pages can also set hide_from_sidebar = true in their own front-matter to hide a single page without affecting siblings.


Page backgrounds

basalt has two independent background-image features:

FeatureScopeConfigured in
Page backgroundper-pageextra.* in page (or section) front-matter
Sidebar backgroundsite-wideextra.* in config.toml

Both are off by default. Both run non-remote images through Zola's resize_image at build time. Sources are resized to ≤ 1920 px wide and converted to WebP (quality 85).

Page background

Set in front-matter on the page that wants a backdrop.

Single image, both themes:

[extra]
bg_image    = "content/images/my-photo.jpg"
bg_position = "center top"   # optional
bg_opacity  = 0.4            # optional; 0.0 = invisible, 1.0 = no overlay

Per-theme variants:

[extra]
bg_image_light = "content/images/light-photo.jpg"
bg_image_dark  = "content/images/dark-photo.jpg"
bg_position    = "center"
bg_opacity     = 1.0

The browser swaps the image when the user toggles the theme. No extra JS.

KeyTypeDefaultDescription
bg_imagestringBackdrop for both themes when no per-theme variants are set.
bg_image_lightstringOverrides bg_image for light theme.
bg_image_darkstringOverrides bg_image for dark theme.
bg_positionstringconfig.extra.bg_default_position"center"CSS background-position.
bg_opacityfloat0.50.0 = invisible, 1.0 = no overlay.

page.extra takes priority over section.extra. A section's _index.md can set defaults inherited by all pages; individual pages override.

Set in config.toml under [extra]. Applies site-wide.

[extra]
sidebar_bg_image       = "content/images/sidebar.jpg"
# or per-theme:
sidebar_bg_image_light = "content/images/sidebar-light.jpg"
sidebar_bg_image_dark  = "content/images/sidebar-dark.jpg"
sidebar_bg_position    = "center"
sidebar_bg_opacity     = 1.0
KeyTypeDefaultDescription
sidebar_bg_imagestringSidebar backdrop, both themes.
sidebar_bg_image_lightstringOverrides for light theme.
sidebar_bg_image_darkstringOverrides for dark theme.
sidebar_bg_positionstring"center"CSS background-position.
sidebar_bg_opacityfloat1.0Default 1.0; site-wide images are usually pre-tuned.

Path resolution

  • content/images/foo.jpg resolves from the project root.
  • static/img/foo.jpg resolves from the project root.
  • https://example.com/photo.jpg passes through unchanged; not processed.

If the source image is narrower than 1920 px, the original width is used (no upscaling).

Implementation notes

templates/base.html emits a single inline <style> block when any background feature is active. If neither feature is used, the block is omitted entirely.

sass/_backgrounds.scss reads --page-bg, --page-bg-position, and --page-bg-overlay and applies them to body. sass/_sidebar.scss reads the --sidebar-bg* counterparts and applies them to .sidebar.


JavaScript hooks

basalt exposes two extension surfaces on window.basalt. Both are safe to call before the main bundle loads: the palette queue drains on first open, and the theme API is set up by the inlined theme.js before any deferred script runs.

Theme API

theme.js is inlined in <head> and runs before the first paint. window.basalt.theme is the canonical API:

window.basalt.theme.toggle();      // flip between light and dark
window.basalt.theme.set('dark');   // set explicitly
window.basalt.theme.current();     // returns 'light' | 'dark'

The theme persists to localStorage under the key theme. The data-theme attribute on <html> reflects the current value; all CSS custom-property selectors read from it.

Palette API

Register additional command-palette actions dynamically:

window.basalt = window.basalt || {};
window.basalt.palette = window.basalt.palette || { _queue: [] };
window.basalt.palette.register({
  id:     'my-action',
  label:  'Do a thing',
  group:  'Actions',
  glyph:  '',
  action: () => doAThing(),
});

Navigate to a URL instead of calling a function:

window.basalt.palette.register({
  id:    'rss',
  label: 'Subscribe to RSS',
  group: 'Links',
  glyph: '',
  href:  '/atom.xml',
});

The full PaletteAction type:

FieldTypeNotes
idstringUnique identifier.
labelstringPrimary display text.
groupstringSection header in the palette.
action() => voidCalled on selection. One of action, href, or js is required.
hrefstringNavigate to URL on selection.
jsstringDotted window.* path called on selection.
glyphstringOptional single-character icon.
substringOptional secondary line below the label.
hintstringOptional keyboard shortcut badge shown right of the label.

Actions can also be declared statically in config.toml without writing JS:

[[extra.palette_actions]]
id    = "rss"
label = "Subscribe to RSS"
group = "Links"
glyph = ""
href  = "/atom.xml"

config.toml declares palette items only — never hotkeys. key and inHotkeys fields on palette_actions are stripped at load time. To bind a key, register the command from static/js/main.js:

window.basalt.registerCommand({
  id: 'subscribe',
  label: 'Subscribe to RSS',
  group: 'Links',
  href: '/atom.xml',
  key: 'r',          // hotkeys live in JS only
});

Group ordering

Palette groups render in the order they're encountered. Pin or hide groups via [[extra.palette_groups]]:

[[extra.palette_groups]]
id     = "Go"
weight = 0          # floats to the top

[[extra.palette_groups]]
id     = "Posts"
weight = 10

[[extra.palette_groups]]
id     = "Posts"
label  = "Writing"  # rename a group at render time

[[extra.palette_groups]]
id     = "Debug"
weight = -1         # hide

Lower weights render first. Groups not listed keep their implicit order at the end.

Lazy loading

basalt's built-in modules (palette, comments, KaTeX, mermaid, asciinema, lightbox, carousel) are dynamic-imported inline from src/main.ts using two small helpers:

  • onLoad(selector, label, importer) — fire once at init if any matching element exists.
  • onVisible(selector, label, importer) — fire when the first match scrolls into view (IntersectionObserver, 200px rootMargin).

Sites that need their own lazy bundles can copy the pattern from src/main.ts — there's no separate registration API to learn. Each importer is a thunk; failures are logged via console.warn and don't crash the page.

Re-init after dynamic injection

DOM walkers that inject markup at init time (color chips on inline <code>, copy buttons on <pre.giallo>) register themselves with the re-init registry. Site code that injects code after first paint (lazy comments, server fragments, view transitions) can call:

window.basalt.reinit(rootElement); // omit rootElement to walk the whole document

Each registered walker is idempotent — re-calling on already-processed nodes is a no-op.

Loading order

The main bundle loads as <script type="module"> (deferred by default). Heavy modules are dynamic-imported only when the relevant DOM is detected. theme.js is inlined into <head> so theme preference is applied before first paint and stays free of bundler concerns. Site-level static/js/main.js (if present) loads defer at body end via basalt's base.html.


Code blocks

basalt renders code blocks from standard markdown triple-backtick fences. There's no shortcode; the language identifier after the opening fence is enough.

```rust
fn main() {
    println!("hello");
}
```

Renders as a syntax-highlighted block with a copy button in the top-right corner. Drop the language identifier for a plain block with no highlighting. The copy button still appears.

How highlighting works

basalt uses Zola's built-in giallo syntax highlighter with style = "class" in config.toml. Tokens render as CSS classes (.z-keyword, .z-string, etc.) rather than inline styles. basalt ships static/giallo-light.css and static/giallo-dark.css, generated by Zola at build time and linked from head.html. theme.js toggles the disabled attribute on one of them when the theme changes. No re-fetch.

How the copy button works

src/copy-code.ts walks every pre.giallo at module load and appends a <button class="codeblock-copy"> to each one. The button is only injected when navigator.clipboard?.writeText is defined. JS-off users and old browsers see clean code blocks with no copy affordance.

pre.mermaid and other non-giallo <pre> blocks are excluded by the selector.

Customising the chrome

Override .codeblock-copy styles in your own SCSS. The defaults live in sass/_code.scss and use --code-bg, --soft-border, --muted, and --term from sass/_tokens.scss. Reskin via tokens for the cheapest customisation.


/now/ data schema

The /now/ page is rendered by templates/now.html from data/now.json in your site root. Place the file at <project-root>/data/now.json.

{
  "status": {
    "label":   "building something",
    "blocked": "too much to do, not enough time",
    "mood":    "cautiously optimistic"
  },
  "currently": [
    "Working on the new site",
    "One bullet per thing you're actively doing"
  ],
  "reading": [
    { "kind": "book",  "title": "Title here",     "meta": "Author, format"     },
    { "kind": "audio", "title": "Audiobook title", "meta": "Author, commute loops" },
    { "kind": "watch", "title": "Series title",    "meta": "current season"     }
  ]
}

Field reference

FieldTypeRendered as
status.labelstring"doing" row in the status pane
status.blockedstring"blocked" row, styled heat-high (red)
status.moodstring"mood" row, styled heat-med (amber)
currentlystring[]Bulleted list in the "currently" pane
readingobject[]Rows in the "reading · watching" pane
reading[].kindstringLeft-aligned label (book, audio, watch, or anything)
reading[].titlestringMiddle column
reading[].metastringRight-aligned muted metadata

The "tinkering with" pane and "latest post" pane on /now/ are pulled from Zola's section graph automatically (projects/ and blog/ respectively). No now.json fields needed for those.

TITLE
typography
Editorial em accent, semantic-tag defaults, font stack overrides.
shortcodes
Every shortcode basalt ships, with a live demo and the source for each.