I made a Zola theme and now you have to deal with it

A monospaced-by-default, javascript-optional Zola theme. Two consumer sites running on it, two personas, one shared engine. Here is what shipped in v1 and the part that took six rewrites.

contents · 5 sections
  1. What ships in v1
  2. Personas
  3. What it costs
  4. What's on the roadmap
  5. Using it

I spent too many weekends on a Zola theme, and here we are.

The result is basalt, a Zola theme with small page sizes, readable type, and defaults that don't make me argue with the config file. Two sites run on it: this one (terminal-themed, vaguely angry) and timsiggins.com (editorial, vaguely respectable). Same theme, two completely different identities, one shared [extra.persona.*] token table.

Want to poke at it? Source is at codeberg.org/ttsigg/basalt, the project page lives at /projects/basalt/, and the README covers install.

What ships in v1

Small pages. Under 40kb per page on almost every route, fonts excluded. The footer prints the actual number on every load. No tracking pixels, no analytics blob, no cookie banner. Body copy is variable Inter (or Public Sans, on ts.com), headings are Space Grotesk (or Newsreader, ts.com), code is JetBrains Mono. All variable WOFF2, Latin subset, cached for a year.

No build step in the consumer repo. The JS is a Vite bundle that ships from the theme's static/dist/ as a committed artifact. Consumer sites don't run pnpm install. If you want to recompile, the theme repo has a justfile.

Light and dark, no flash. Theme preference lives in localStorage. Boot script is inlined in <head> and runs before paint, so there's no wrong-theme-flash even on a slow connection. Press t to toggle.

Command palette on /. Searches post titles and bodies via Zola's built-in index. Lazy-loads on first keypress, about 3kb on the wire. Ctrl+K works too if your muscle memory disagrees with the vim default.

Asciinema and KaTeX, pay-per-use. Both only load on pages that use them. A post with no math pays nothing for KaTeX. A post with no terminal recording pays nothing for asciinema.

Mastodon and Bluesky comments. Opt-in per post, lazy-loaded on scroll-into-view. No third-party script until the reader gets there. No cookies. No consent banner. Live demos: Mastodon, Bluesky.

Responsive images out of the box. The image shortcode emits a srcset with WebP variants via Zola's resize_image() at build time. Optional <dialog> lightbox, optional left/right float, all driven by one shortcode.

Eleven shortcodes, all documented. alert, collapsible, em, toc, image, carousel, asciinema, katex, mermaid, project, plus pullquote and epigraph for editorial breaks. Every one has a no-JS fallback. Full demo lives in basalt content/options/.

Personas

The thing I rewrote six times is the persona system. Both consumer sites speak the same theme but render hella differently:

Tokenauxdev (this site)timsiggins.com
--font-textInterPublic Sans
--font-displaySpace GroteskNewsreader
--ornament▍ ▍ ▍
--anchor-glyph§
Accentterminal greeneditorial indigo
Pullquote fontmono italicserif italic

Both sites import the theme once via SCSS aggregator, then layer their own [extra.persona.*] token table for colours and fonts, plus a templates/partials/site-head.html for any inline head injection. That's the whole customisation surface.

What it costs

Rounded, as served, gzipped:

  • Home page HTML: about 8kb
  • Blog post HTML: about 9kb, plus the post body
  • main.css: about 16kb
  • theme.js (inlined, every page): about 1kb
  • Vite main bundle (deferred, every page): about 4kb
  • Lazy chunks (palette, comments, katex, asciinema): 1-3kb each, only on demand
  • Fonts, first visit: about 110kb across three WOFF2 files, then cached for a year

A cold first visit is comfortably under 200kb. A repeat visit, once fonts are cached, is about 25kb. The site stays in the 512kb.club green tier on every route. If you find a route that doesn't, file an issue.

What's on the roadmap

I ship theme updates when I remember to, not on a cadence. Loosely ordered by how much I want each one:

  1. A "year in review" archive page. Group posts by year, sparkline of post frequency, link out. One Tera template and a small SCSS file.
  2. Tag-prefix search. The palette searches title and body; I want a #rust-style prefix that filters on tags too.
  3. A "printable" view. Strip the sidebar, flatten the colours, reflow to a paper width. Mostly for when someone sends me a post and I want it on my Kindle.
  4. Webmention sender + receiver. Mastodon and Bluesky comments are neat; a webmention or two would be neater.

Anything else is a maybe. Repo issues are open.

Using it

git clone https://codeberg.org/ttsigg/basalt my-blog-theme
git clone https://codeberg.org/ttsigg/auxdev.net my-blog
cd my-blog
ln -s ../my-blog-theme themes/basalt   # symlink for now; submodule wiring on the way
just setup
just serve

Point config.toml at yourself, drop your content into content/, and just build. The DEPLOY notes for Codeberg + Cloudflare Workers live in the auxdev repo.

If you do something neat with it, let me know.

← previous
Comments, loaded from Mastodon