Zola how-to

Table of contents

Zola is a static site generator written in Rust. Zola has a pretty good documentation to get you started quickly.

As a reminder, this will be how to use Zola to create a blog that fits my own needs. It might not be relevant, or even applicable to someone else's needs. The documentation seems to suggest to keep blog posts in a content/blog directory, which would output a similar path, and the way to achieve something different is mostly by creating theme files. My approach has been quite different than what seems to be the recommended one.

Installation

git clone https://github.com/getzola/zola.git
cd zola
cargo install --path . --locked
zola --version # should return "zola 0.19.2" or similar
# copy binary somewhere in $PATH
cp target/release/zola ~/.cargo/bin/zola

Alternatively, we can download the latest binary somewhere in $PATH and make it executable.

Zola comes with a few build-in commands (see all with zola help):

So, to create our first Zola project:

mkdir myproject
cd myproject
zola init

For demonstration purposes, we will just press enter to every question, accepting the default values. Now, inside the directory myproject we have the following structure:

tree myproject
.
├── config.toml
├── content
├── sass
├── static
├── templates
└── themes

All the values we accepted by default in the previous step are in config.toml:

base_url = "https://example.com"

compile_sass = true

build_search_index = false

[markdown]
highlight_code = false

[extra]
# Put all your custom variables here

Create a blog

In this example, we will not use sass or themes, so we can rmdir sass themes to delete the directories. We also need to change the config.toml to reflect this:

base_url = "/"
compile_sass = false
build_search_index = false

[markdown]
highlight_code = true

[extra]
# Put all your custom variables here

We will use base_url = "/" so to get relative links when we build the site.

Zola uses the Tera templating engine, which works very similarly to jinja2. We can create a base template and extend it with the {% extends <template name>.html %} syntax and mark where the content will be injected with {% block content %}{% endblock %}:

<!DOCTYPE HTML>

<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>My Zola blog</title>
</head>

<body>
  {% block content %}{% endblock %}
</body>
</html>
{% extends "base.html" %}

{% block content %}
<h1>Hi</h1>
{% endblock %}

We can serve the site with zola serve and watch the changes at 127.0.0.1:1111

Blog posts

Everything that is considered content goes to the content directory. Any subdirectory of content will have the respective path. For example, if we decide to create all our blog posts in content/articles, and we create the file content/articles/post-1.md, the URL of the post will be 127.0.0.1:1111/articles/post-1. Similarly, if we create an "About" page in content/about.md, it will be displayed at 127.0.0.1:1111/about.

However, for this example, we want flat URLs for everything. Every post should have a path of <base url>/my-post and every page a path of <base url>/mypage.

To achieve this, we will add our blog posts in content without a directory structure.

The first thing we need is to create our first blog post with some frontmatter. Frontmatter in Tera can have the form of yaml frontmatter:

---
title: my title
date: 2024-01-01
description: my first blog post
---

although it is recommended to use toml-like frontmatter:

+++
title="my title"
date=2024-01-01
description="my first blog post"
+++

I will be using the yaml frontmatter format, for more portability of the blog posts between static site generators.

So, let's create that blog post:

---
title: First post
date: 2024-01-01
---

## My first Zola post

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla vitae est turpis.

If we try to see our post at 127.0.0.1:1111/post-1/, we will see instead the "Welcome to Zola" page, with the following message:

You're seeing this page because we couldn't find a template to render.

To modify this page, create a page.html file in the templates directory or install a theme.

So, we need to create one more template, templates/page.html:

{% extends "base.html" %}

{% block content %}
<h1>{{ page.title }}</h1>
<p>Published on: {{ page.date }}</p>
{{ page.content | safe }}
{% endblock %}

And now our post is visible at 127.0.0.1:1111/post-1.

Creating the post index at the home page

We will rewrite content/index.html:

{% extends "base.html" %}

{% block content %}
<h2>My blog</h2>
<ul>
{% for p in section.pages %}
  <li><a href="{{ p.permalink }}">{{ p.title }}</a></li>
{% endfor %}
</ul>
{% endblock %}

Creating static pages

Let's say we need some "static" pages, such as an "About" page, maybe a "Services", or "Projects", or really whatever. Following the previous flat directory approach, let's add the "About" page in the content directory:

---
title: "About"
date: 2024-01-01
---

Hi, this is the "About" page.

To add a link to homepage in every page, we modify templates/base.html:

<body>
  <nav><a href="/">Home</a> | <a href="/about/">About</a></nav>
  <hr>
  {% block content %}{% endblock %}
</body>

Now everything works correctly, and all pages are linked and resolve correctly. However, there are two problems:

  1. The "About" page, following the templates/page.html template, displays the day published, which might not be desirable for static pages.
  2. In the index page, "About" is listed in the list of blog posts, which is definitively counter-intuitive.

The first problem is very easy to solve: We edit the templates/page.html page as follows:

{% extends "base.html" %}

{% block content %}
<h1>{{ page.title }}</h1>
{% if page.date %}
<p>Published on: {{ page.date }}</p>
{% endif %}
{{ page.content | safe }}
{% endblock %}

Then, we completely remove the date: 2024-01-01 frontmatter variable from the about.md file, and this works perfectly! The page doesn't display a date anymore.

Alternatively, we can create a new template, templates/static.html:

{% extends "base.html" %}

{% block content %}
<h1>{{ page.title }}</h1>
{{ page.content | safe }}
{% endblock %}

and make the "About" page use it by adding it in the frontmatter:

---
title: About
date: 2024-01-01
template: "static.html"
---

Both of these ways correctly resolve the problem (1). To resolve (2), however, we need to find a way to separate blog posts from static pages. Let's create a subdirectory in the content directory: content/pages and move content/about.md there. Now the page is served from <base url>/pages/about/, which is still not what we want. So, let's edit the frontmatter of content/pages/about.md:

---
title: About
path: about
---

And now, everything works as expected!

Configuring the content directory

So far so good, but there is a slight problem. As we go on adding more posts in our content directory, the index page that lists the posts displays them in chronological order. That is, the oldest is first and the newest is last in the list. This is not the usual way people set their blogs and it might not be the desirable behaviour.

As far as I could tell, without any intervention, Zola orders texts chronologically according to their timestamp. We want to use the date metadata in our frontmatter and have the post list display them in a reverse chronological order (newest first).

For this, we will add an content/_index.md file in our content directory. The frontmatter of this file will set some preferences for the directory; the underscore in the file name is there so that it always shows up first in our directory.

+++
sort_by = "date"
+++

And that takes care of it! We can add all sort of configuration options in the _index.md file, such as what template the pages should use (if different than the default, page.html, what template the index file itself should use, and more. Basically, the _index.md file creates a section, and, in a complex site structure, each section can have its own configuration parameters.

Static files

If we want to add a CSS style, we will add it in the static directory (ex., static/main.css and link it in the base template simply as <link rel="stylesheet" href="/main.css">. Similarly, we can drop images in the static directory and link them as <img src="/myimage.jpg" alt="my image"> (or ![my image](/myimage.jpg) in markdown).

Additional features

Table of contents

Zola offers template variables for a table of content out of the box. We want to use the page.toc variable and we want to start from the h2 header (as h1 is the title):

<strong>Table of contents</strong>
<ul>
  {% for h2 in page.toc %}
  <li><a href="{{ h2.permalink | safe }}">{{ h2.title }}</a>
    {% if h2.children %}
    <ul>
      {% for h3 in h2.children %}
      <li><a href="{{ h3.permalink | safe }}">{{ h3.title }}</a></li>
      {% endfor %}
    </ul>
    {% endif %}
  </li>
  {% endfor %}
</ul>

This could go in the page.html template, to work only with posts.

RSS feed

Zola also has built-in the option for feeds. In config.toml:

base_url = "/"
compile_sass = false
build_search_index = false
generate_feeds = true
author = "John Doe"

[markdown]
highlight_code = true

[extra]
# Put all your custom variables here

This will automatically generate a feed at 127.0.0.1:1111/atom.xml.

Custom 404 page

Zola will use the templates/404.html if it finds one:

{% extends "base.html" %}

{% block content %}
<h1>404 - Page not found</h1>
<p>Return to <a href="/">Home</a></p>
{% endblock %}