The growing pains of working with Zola will lead me to make new choices for maintaining this website.
Published on
For anyone reading this website, they will know that I have been maintaining it and publishing on it using Zola, a simplistic, single-binary with very few dependencies. Up until now, I appreciated Zola for being small, lightweight, and written in Rust. You can use ldd
to see how little Zola depends on externally, too, and the dependencies make sense given what Zola provides.
$ ldd $(which zola)
linux-vdso.so.1 (0x00007ffff32e5000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007f8c281c5000)
libm.so.6 => /usr/lib/libm.so.6 (0x00007f8c26918000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007f8c26731000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f8c28210000)
However, I have some growing pains with Zola, and I think it's time I part ways to move onto bigger and better things slowly. My decision may or may not be set on using Hugo instead.
Zola is a nice and light-weight alternative to things like Jekyll or Pelican. Unlike both Jekyll or Pelican, Zola isn't written in Ruby or Python, and at first glance, the performance differences are notable. Pelican, Jekyll, or anything that seemingly depends on a dynamically-typed language, will end up being larger to clone in build pipelines.
My average build pipeline for my website is that I build it with a tool to generate flat HTML pages, then submit changes through a Git repository pipeline. The pipeline absorbs the files and uploads them to a page-hosting CDN network, where files are served when visitors travel to my domain.
The CI pipeline can automate the building process by using Docker images to download Pelican, Jekyll, Hugo, or Zola, and processing the end-result files, which are then used for the CDN upload instead. I don't personally do this anymore, as Git hosts are usually victim to cryptoware issues with offering free CI minutes. I just build it myself on my computer.
Pelican and Jekyll are slightly larger in size, and take more time to download. Some could take hundreds of megabytes; but Zola is a mere 20 megabytes or so. The CI pipeline will be faster, because downloading Zola is faster, and Zola is also faster because it's a native binary, not a virtual machine language like Python or Ruby. Mayhaps Pelican and Jekyll can do C-level foreign function interfaces to make their programs faster, but that most likely won't beat out Zola still.
Zola has an image processing library as well. Pelican and Jekyll do not. Sometimes you need to prepare your images for the web, and doing it manually really kills the momentum of web writing. This is a pain point not solved, although Jekyll/Plugins may exist, but the functionality isn't native.
At runtime, Zola can collect images and do basic things like resize operations. Getting this right is hard, and doing it in a way that isn't obtrusive is also difficult too. Images are critical for the web, but being able to resize, rotate, adjust brightnesses and perform all sorts of image-based tricks is not an easy task for a writer to manage and mess around with, at least not without impeding on writing progress.
It is very possible to generate blogs, photography or art portfolios, or anything inbetween using Zola. Chunking thumbnails out of images and using those thumbnails for index pages is a fairly trivial process with Zola. For any other static site generator, prepare for breaking plugins and more (experience could vary).
The templating system is decent enough to use, and making shortcodes is a peaceful process. The Zola API has a simple system which breaks components down into basic Rust structures that you can read and iterate over in a way that makes sense. Folders become sections, sections have pages, you iterate over all pages and generate the information you need from your core data objects.
The templating system follows that of a parental inheritance system using block-out sections. You inherit pages by declaring a parent template, and you block off sections in your children templates. Re-usable templates are important to create sane page structure, and this system Zola uses with the Tera template engine is pretty decent.
Normally as you develop on static site generators, it's nice to see what you're writing side-by-side. You can do that using Zola, as it provides that functionality. All Zola previews come with an AJAX-based live-reloading package that, when you save your content files, Zola observes a file write event and triggers a re-compilation of your content. The AJAX listener then reloads the window upon re-compilation and views your page instantly. With it, you can very much see your progress as you write side-by-side.
This does come in handy in general for writing, but also for debugging your web pages when fixing up your shortcodes.
Years ago for fun I built my own static site generator in Racket. It was a pain in the butt and I couldn't get many things I wanted down-pat. I switched to Zola for simplicity to speed up my CI/CD pipelines, because downloading Racket Docker images took too long / ate up too many of my precious minutes. I was satisfied with Zola for the most part.
But, after using it for a few years, I've let my site rot, and in some ways I feel like I almost have technical debt by simply using it.
There are two ways of organizing your files in Zola, and unfortunately I feel like it's almost too many.
There's the no-folder structure you can go for:
content/
blog/
hello-world.md
Or there's the sub-folder method, which is useful for when you want assets next to your content.
content/
blog/
hello-world/
index.md
trip_to_italy_photo.jpg
picture_of_my_cat.jpg
One of these methods is quick and dirty; the other method is for actual writing with photos. But transitioning from plain file to a directory-with-files is more annoying than it should be. Sometimes you want to include photos, sometimes you don't. It might have been better if they went with the folder structure by default, and ignored single-files altogether.
You can now say "why not just put all your pictures in a static folder if you're going to use single-file blogs?" First, because that's problematic and doesn't help your organization at all. Static folders for dispatching assets is really only good for images (or other assets) that will re-appear all over your website. By stuffing your static folder with assets, you'll never remember where your pictures of your cats are.
The single-file strategy, in practice, just isn't efficient. For me to have technical knowledge to be able to move files into different sections is fine; but for non-technical people, it's gotta be more annoying than not. You'd have to close the file, rename it in your file manager, make a folder, then move the file to that location, then continue to re-edit it. It should just be the behavior by default to only use sub-folders with index pages.
Yes, sure this is a simple mkdir
command followed by an mv
command. It still requires you to close the buffer by saving it to disk then re-opening it again. I am still a fan of simply removing single-file structure entirely.
It would be nice if the Zola binary somehow provided a way to simply start a new page entirely. It's an observation of mine that I really just want to hop into a new file instantly; something that would boot up my preferred editor into a new folder with the option to provide a title. Zola is not a program that helps with that.
If you wanted to keep your wits about you, and you use Git, you might take note of the fact that your file stat
s don't get preserved into Git at all. Permissions nor datestamps are preserved when things are checked into Git, and when you do a full-clone, all file metadata is based on when a clone or pull was last received (since it's directly manipulating files). So, basically you can't rely on Git to preserve information like "what's my most recent blog post?".
In order to not go crazy, you might want to use a datestamp format in your file names. I use a yyyy-mm-dd-title
structure for my folders, because if not, you might go insane. However, there's no clear guidelines on how to do any of this, because Zola does not provide guidance built-in, at least not without doing some of your own research at first. Even in their first-steps tutorial, they don't really acknowledge a clean way of preserving metadata for the users' sake, their blog example is literally this for a new blog:
├── config.toml
├── content/
│ └── blog/
│ ├── _index.md
│ ├── first.md
│ └── second.md
├── sass/
├── static/
├── templates/
│ ├── base.html
│ ├── blog-page.html
│ ├── blog.html
│ └── index.html
└── themes/
Writing a blog like this, it's clear files titled first.md
or second.md
aren't going to cut it. You have no way of knowing which one came first and in which order, at least not without observing the titles. But the minute you break out of that naming schema, you will be lost when staring at a folder with thousands of names and no date preservation of any kind.
It might be nice if Zola provided a command that would allow you to rectify for this shortcoming; something that would be as simple as zola write blog <title>
and it generates a folder with the page to edit. It could even fill in the metadata which we normally keep at the top of Markdown files for us, so the user doesn't have to perfectly know which date format is used for their seemingly arcane setup.
# a Zola markdown file in my setup
+++
title = "My First Title"
description = "Some description goes here"
date = 2023-04-13
+++
Nothing about this screams crazy. Sure I could write it myself as a basic script in Shell, but this is something so trivial I feel it should be provided by the application, and not me. I shouldn't have to patch weird shortcomings like this.
It could be argued that because you can name and put files anywhere in Zola, and it iterates recursively over your file structure, it wouldn't make sense for a Zola command to be made like this. But, if we're writing websites with structured sub-folders like content/blog/
, then it's pretty clear a command would be of some help. Hopefully nobody is out there writing file targeting the root of their domain, so ... Yeah, maybe this isn't a totally outlandish idea.
At least in my head, a command like zola write <folder> <title>
would be good enough to at least get over some of the busy-work of folder-slugging the title name, filling in the date, and getting the folder with a file prepared. It could even immediately boot up your editor (if you happen to use the $EDITOR
variable maybe...) so you can jump right into the file. It would be nice, make it more user-friendly, and altogether make it not a hassle for me to juggle files left and right.
I don't really have all that much a vendetta against Markdown, but there are times where it feels like I write shortcode to supplement the fact that Markdown provides very little functionality. It wasn't until I wrote a shortcode that better enables embedding images that I felt my articles were much better. I don't expect Zola to throw the kitchen sink at me with all of it's bells and whistles, but to write Zola means you'll inevitably have to learn shortcoding yourself.
Take for example this image.

It embeds the image as Markdown usually does, but this is what feels like the absolute bare minimum. Stubbing the alt text field property and linking the image is such lazy nonsense. On top of that, it doesn't even mark it as a lazy-loading image, so users get to load all the data at once, instead of lazily, to avoid major data requests. The user might not be interested in reading all the way through, why make them load images they don't want?
I had to design my own shortcode to embed images that supported adding additional quotes, and make it support title
and alt
properties. I could make it go further and lock in the image width
and height
properties, but that's generally managed by CSS rules anyway.
# image_embed.html
{% set img_url = get_url(path=url) %}
<div class="caption-img-block">
<a href="{{img_url}}" target="_blank">
<img src="{{ img_url }}" {% if desc %} alt="{{desc}}" title="{{desc}}" {% endif %} loading="lazy" />
</a>
{% if desc %}<p>{{ desc | markdown(inline=true) | safe }}</p>{% endif %}
</div>
Hell, this Zola Markdown doesn't even do block quotes at all. The >
operator normally presents a quoted block of text (usually from GitHub issues) and indents it slightly. But if you were to do this:
> Even communism works… in theory.
> Homer Simpson
... you get a one-line sentence. Compared however, to my quote.html
shortcode, which provides author and source capabilities:
Even Communism works... in theory.
Looks much nicer! Maybe not great, but nicer than literally doing the bare minimum.
I get that the CSS is on the end-user themselves, but it feels like many aspects of Zola's Markdown are just unusable, and you'll often find yourself having to write your own shortcodes many times to get more functionality. It might just be that the Markdown spec itself is so inflexible for real websites that it's only good enough for the bare minimum focus, that being on writers. But writers themselves will probably never be caught using Zola if it isn't user-friendly enough anyways. That leaves Zola to us - the programmers.
When I said it's best to keep your cat and vacation photos next to your content file, it's useful, because it isn't in some weird file far away that you have to dig through. In your Markdown, when you include images, you can simply do 
, and Zola expands that path when publishing the file.
This is less helpful, however, later on when trying to do shortcode hacks with these co-located files. The Zola API is sort of lacking, and it isn't great at doing a very specific thing I am trying to implement.
Medium, Substack and other such websites allow you to upload an image to put at the top of your writing piece. This is a simple and non-obstructive way of adding a nice touch on top of an article. In Zola, this is not so easy to replicate the behavior of. And I'll show you why.
Since social media is common, a nice thing to do to attract visitors is by sharing pages on social media platforms to grab people's attention. There's text messages (Toots, tweets, etc), then there's those with pictures, then there's with links, and lastly, one with both a picture and a link. The picture+link combination grabs the most web traffic, because the picture can grab eyeballs and draw curiosity.
To do this, most social media websites implement the Open Graph Protocol for retrieving metadata about a website upon visit. Most commonly, they'll look for things marked with property="og:image"
and use that source as the image. This is required to be in the head
section of an HTML page, which generally is the best place to store metadata.
Zola is a little tricky, because in part: when rendering blog pages, usually your head
section is already declared by your template, due to the inheritance properties of Tera templates. You override the title and maybe the meta description, but not much more usually. You can set a section with Zola by doing {% block meta %}
or something the like, but as I've learned, that's not the big issue. The real issue is getting Zola to actually locate an image to do this with.
Open Graph requires additional information in order to pass image width and height. Zola can get this, but it's problematic. Zola requires a hard file path to open and read metadata from. If it cannot find a file, it won't build the website, period. The function I'm referring to in Zola is get_image_metadata(path="...")
. To use this function, you must provide a static path to an image. This is the most annoying part about, and I hate it.
Let's say I have this setup in a blog post I'm about to revolutionize the world with:
+++
title = "Get Ready to Change Your Way of Thinking"
description = "New scentifically-proven ways of changing your way of thinking"
date = 2023-04-13
+++
I have an image stored in stunning_image.jpg
that I want to use as the Open Graph image. I could use a shortcode macro of some sort to stuff it into the HTML.
# inside templates/shortcodes/banner.html
{% set data = get_image_metadata(path=some_path) %}
{% block metadata %}
<meta property="og:image" content="{{ some_path }}" />
<meta property="og:image:width" content="{{data.width}}px" />
<meta property="og:image:height" content="{{data.height}}px" />
{% endblock %}
It turns out that, inside shortcodes, you cannot override user-defined blocks like this. Whether or not this is by design, or because it's probably clear insanity (a shortcode may or may not reference a non-existent block, so it would do nothing-work needlessly), or because the block functionality simply isn't exposed. It feels weird that other key features like set
or if
, or using for
works, but block
will not.
Okay. No big deal. Let's move on.
It is my belief that maybe this image that is critical to the success of the article shouldn't be in a compile-time text macro, and should maybe be somewhere in the article metadata instead. To support this, we might consider using the [extra]
section of the content TOML section instead.
The top section of a Zola content file is treated as TOML and is read as literally as can be, give or take. Strings are strings, other things are raw types, and then there's things like dates which Zola enforces. But, you can add extra things via the [extra]
section, so maybe we can stuff an important article splash image in there.
# article contents
+++
title = "Get Ready to Change Your Way of Thinking"
description = "New scentifically-proven ways of changing your way of thinking"
date = 2023-04-13
[extra]
image = "stunning_image.jpg"
+++
Then, by adjusting our template to take into account for this seemingly extra variable, we do:
# templates/some_template.html
{% if page.extra.image %}
{% set data = get_image_metadata(path=page.extra.image) %}
{% block metadata %}
<meta property="og:image" content="{{ some_path }}" />
<meta property="og:image:width" content="{{data.width}}px" />
<meta property="og:image:height" content="{{data.height}}px" />
{% endblock %}
<img src="{{ page.extra.image }}" />
{% endif %}
This will not pass in any capacity. Can you guess the error? It's that stunning_image.jpg
cannot be found in any way.
When Zola builds your website, it transfers files to a folder structure that makes sense for the web. To prepare URL linking of random assets of the web, we use get_url(path="...")
to get a public-net facing URL for an asset file. But Zola cannot use that location of the compiled asset for the metadata function. It needs a pre-compilation asset path instead. Here's another gripe I hate.
+++
title = "Get Ready to Change Your Way of Thinking"
description = "New scentifically-proven ways of changing your way of thinking"
date = 2023-04-13
[extra]
image = "content/subfolder-with-extra-stub-data/stunning_image.jpg"
+++
Okay, so this works. Sort of. Except we have an issue: this path isn't valid when it hits the web. We need a valid path for when it exists live, but at the same time, we still need a valid path to where it sits in our repository. So you would need a combination of a path to where the file is now, and then a path to where the file is going to live when it's uploaded.
+++
title = "Get Ready to Change Your Way of Thinking"
description = "New scentifically-proven ways of changing your way of thinking"
date = 2023-04-13
[extra]
base_image = "content/subfolder-with-extra-stub-data/stunning_image.jpg"
live_url = "subfolder-with-extra-stub-data/stunning_image.jpg"
+++
# templates/some_template.html
{% if page.extra.base_image %}
{% set data = get_image_metadata(path=page.extra.base_image) %}
{% block metadata %}
<meta property="og:image" content="{{ some_path }}" />
<meta property="og:image:width" content="{{data.width}}px" />
<meta property="og:image:height" content="{{data.height}}px" />
{% endblock %}
<img src="{{ page.extra.live_url }}" />
{% endif %}
For something as simple as attaching a basic image like many of the other big-name publishing platforms, this is a hassle. I am not someone who's about to go scour all the other Zola-based open source repos out there to see how they accomplish it, but seeing how I had to do it in template code, this is pretty awful. Not only do I have to enter where my file lives, because Zola can't perform a co-location of it's true location for me, now I have to "guess" where the file is going to live when it goes live.
Not that it's much to predict what the live URL is going to be, but the fact that I even have to do it at all, is a pain. I am not a fan of doing double-work like this.
As someone using Zola for years now, I appreciate Zola and their hard work of getting Rust into the static site generation world. It's the only one there, as far as I can tell. Nobody's going to compete with them in that front... Except Hugo.
Hugo is a Go-based SSG which is a little more sturdier and community-loved, I will say. Everything I've seen about Hugo is that it's great. I see way more Hugo love than I do Zola. The downside to Hugo is that it might come with more bloat and end up being larger in the long run for downloads, because it's Go, Go tends to stuff binaries with a lot more (runtime, allocator, garbage collector, static libraries, etc).
At this stage, after writing this (which took me the better part of a day), it's hard for me to say if Hugo is the right fit. I want something that I can maintain, and may end up developing my own solution later on in life with more tools that I really want to flesh out myself. Hugo might be an important switch in the now, until I can solve my problems later in life.
So in short: I'm frustrated by Zola, and looking for options. If you have SSGs you use that you love, feel free to reach me on my Mastodon account and show me which SSGs you love.