Trying out Zensical

2026-01-10 | Updated: 2026-02-06

Today I am trying to set up a personal site / blog using the static-site-generator zensical. So far the deployment has been pretty easy. I am using the zensical serve command for production of course, even though it is only meant for dev purposes, but it just makes it so easy to deploy on docker... Right now I am trying out how subdirectories like this blog directory work. At first I was a bit sceptical because I have tried several static site generators in the past. They were either too complicated to set up (I can write my html manually at that point), or their defaults were not what I envisioned. Zensical, so far, feels very clean, easy to use and fast.

Here is the stack config I used for docker swarm (good excuse to try out zensical's code blocks):

version: '3.8'

services:
  zensical:
    image: ${DOCKER_PROXY:-}zensical/zensical
    volumes:
      - /mnt/appdata/docker/zensical/docs:/docs
    environment:
      - TZ=Europe/Berlin
    networks:
      - web
    deploy:
      update_config:
        order: start-first
      labels:
        traefik.enable: 'true'
        traefik.http.routers.zensical.entrypoints: 'websecure'
        traefik.http.routers.zensical.rule: 'Host(`zensical.lhns.de`)'
        traefik.http.services.zensical.loadbalancer.server.port: '8000'
        traefik.http.services.zensical.loadbalancer.server.scheme: 'http'

networks:
  web:
    external: true
    name: "traefik_web"

A common pattern I like to use, is the DOCKER_PROXY prefix variable, which is empty by default, but which I can override to configure a docker cache proxy.

I think now is the time to try out page metadata. This is useful to, for example to annotate each page with its relevant date. Here I am running into the first problems. I tried to set the pages status attribute, but in the process of restarting zensical (sometimes it doesn't detect changes and has to be convinced), its cache got corrupted. And before you ask, I didn't have order: start-first configured at that time. I only got 404 for my test page at that point. So the caching mechanism doesn't seem to be super robust. To be fair it is only meant for dev purposes anyway and it was easy to resolve by just removing the .cache directory.

Of course I also want the date to be shown on the page itself, not just hide it in the metadata. There doesn't seem to be an out-of-the-box way to do this at the moment though. I would have to put the date into the metadata and manually copy that to some place in the markdown. That of couse is not good enough for me, so I set out to solve this.

I used zensical overrides to override the content.html template. I found all the original templates at https://github.com/zensical/ui/tree/master/src and after configuring the following I could override the file:

zensical.toml
[project.theme]
custom_dir = "overrides"
overrides/partials/content.html
{% if page.meta.lastmod or page.meta.modified %}
  <p><em>
    Updated: {{ page.meta.lastmod | default(page.meta.modified) }}
  </em></p>
{% endif %}

Now every page that has a modified or lastmod configured, gets an "Updated: ..." shown at the top.

Then I improved the template a bit further to show the date (relevant page date) in addition to the lastmod date. Also the lastmod date is now only shown if it differs from the date.

overrides/partials/content.html
{% set lastmod = page.meta.lastmod | default(page.meta.modified) %}
{% if page.meta.date or page.meta.lastmod or page.meta.modified %}
  <p><em>{{
    [
      page.meta.date,
      '' if page.meta.date and page.meta.date == lastmod else 'Updated: ' ~ lastmod
    ] | select | join(' | ')
  }}</em></p>
{% endif %}

After that I noticed another problem. My blog's sidenav was sorted alphabetically ascending. But since I name my files by date, and I want newer posts to appear at the top, I needed an option to reverse the order of some folders. So I did just that:

overrides/partials/nav-item.html
<ul class="md-nav__list" data-md-scrollfix>
  {% set children = nav_item.children %}

  {% if config.extra.reverse_nav is defined and nav_item.title | lower in config.extra.reverse_nav %}
    {% set children = children | reverse %}
  {% endif %}

  <!-- Nested navigation item -->
  {% for item in children %}
    {% if not index or item != index %}
      {{ render(item, path ~ "_" ~ loop.index, level + 1, nav_item) }}
    {% endif %}
  {% endfor %}
</ul>

Now I can configure my blog section, to show its child pages in reverse order:

zensical.toml
[project.extra]
reverse_nav = ["blog"]

A few days later I enabled breadcrumbs:

zensical.toml
features = [
    "navigation.path"
]

I noticed, if I click on a folder, it jumps to the first site in the folder. Or in my case, because I reversed the order, to the last. Another quick fix:

overrides/partials/path-item.html
  <!-- Navigation item with nested items -->
  {% if nav_item.children %}
    {% set first = nav_item.children | first %}
    {% if config.extra.reverse_nav is defined and nav_item.title | lower in config.extra.reverse_nav %}
      {% set first = nav_item.children | last %}
    {% endif %}