02 April 2026
📌 Migrated this site from MkDocs Material to Zola tonight. The trigger was simple: MkDocs feels like a project running on inertia. Faun playing in the headphones throughout.
The site is small. Seven static pages, a notes section with dated posts, two GPG keys served as static files, a feed. Nothing exotic. The migration should have taken twenty minutes. It took longer.
why zola
MkDocs works. The Material theme is polished and the blog plugin covers everything you need. But the dependency chain bothers me: Python, pip, the Material package, pymdownx extensions. A virtual environment to manage. Updates that occasionally break things.
Zola is a single binary written in Rust. No runtime dependencies. No plugin ecosystem to maintain. You copy one file to /usr/local/bin and it works. That's the whole installation. The template engine is Tera - close enough to Jinja2 that it doesn't require relearning anything.
The other reason: I wanted to understand what my site actually does, not what a theme does for it.
the migration script
The structural differences between MkDocs and Zola are mechanical. Frontmatter goes from YAML to TOML. The config file changes from mkdocs.yml to config.toml. The CLI changes from mkdocs to zola. Content lives in content/ instead of docs/. Static files go in static/ instead of docs/public/.
I wrote a Python script to handle the conversion. The non-obvious parts:
My notes had no title: in the frontmatter - the MkDocs blog plugin infers the title from the first heading. The script extracts the first ### heading from the body and uses that. If there's no heading, it formats the date as a human-readable string.
The MkDocs blog plugin was configured with post_url_date_format: yyyy/MM, generating URLs like /notes/2026/03/2026-03-16/. Zola replicates this with a path key in the frontmatter:
+++
title = "proxmox"
date = 2026-03-16
path = "notes/2026/03/2026-03-16"
+++
MkDocs admonitions (!!! warning "title") get converted to Zola shortcodes. The <!-- more --> tag for post summaries works identically in both - no conversion needed.
properdocs
Before settling on Zola I looked at ProperDocs, which someone recommended as a MkDocs replacement. Reading the documentation carefully: it's a fork of MkDocs with the CLI renamed from mkdocs to properdocs and the config renamed from mkdocs.yml to properdocs.yml. The copyright in their own documentation still says Tom Christie. Migration would have been trivial, but the point was to move away from the dependency chain, not rename it.
the theme
Zola is not in the Ubuntu or Debian repositories. Install from the GitHub release binary:
ZOLA_VER=0.20.0
wget -q https://github.com/getzola/zola/releases/download/v${ZOLA_VER}/zola-v${ZOLA_VER}-x86_64-unknown-linux-gnu.tar.gz
tar xzf zola-*.tar.gz
sudo mv zola /usr/local/bin/
I chose the Terminus theme: dark, retro terminal aesthetic, zero JavaScript, perfect Lighthouse score. Installed as a git submodule:
git init
git submodule add https://github.com/ebkalderon/terminus.git themes/terminus
errors worth documenting
Zola 0.19 vs 0.20 config syntax. The theme requires 0.20. In 0.19, syntax highlighting was configured as:
[markdown]
highlight_code = true
highlight_theme = "base16-ocean-dark"
In 0.20 it became a sub-table with a different key name:
[markdown.highlighting]
style = "base16-ocean-dark"
theme = "base16-ocean-dark"
Both keys are required. The theme's CSP partial reads config.markdown.highlighting.style and fails with Variable not found if the section is missing or named incorrectly.
Content Security Policy. The Terminus CSP partial checks config.markdown.highlighting.style even when CSP is not enabled. If you don't need CSP - and for a simple static site you don't - disable it explicitly:
[extra.content_security_policy]
enable = false
Static pages in the root content directory. Terminus renders all pages in content/ on the homepage. Static pages like about.md, books.md, keys.md need to live in a subdirectory - I used content/pages/ - with explicit path values to preserve their original URLs:
+++
title = "About"
date = 2024-01-01
path = "about"
+++
The date is required by the theme's post macro. Any date works for static pages.
Feed template. Zola does not ship a default feed template. You have to create templates/atom.xml yourself. The filename in config.toml must match exactly:
feed_filenames = ["atom.xml"]
feed.xml and atom.xml are not interchangeable - Zola looks for a template named after the filename.
My own templates shadowing the theme. The migration script generated a templates/ directory with custom templates. Those take priority over the theme's templates. Once I moved them to templates.bak/, the theme loaded correctly.
and then...
The site looks better than it did. More importantly, I understand every file in the repository now. The build is one binary and one command. The deploy is the same rsync it always was, just pointing at public/ instead of site/.
The notes section works. The feed works. The GPG keys are where they were. The only thing that changed is what generates the HTML.