Halve-Z

09 Mar 2024

A retro theme for Zola

4 minutes reading time

#Migration

Last year, I decided to migrate my Jekyll website to Zola. The old project was based on the Halve theme and was quite outdated, so I thought it would be a good exercise refreshing it. While porting the old site wasn't hard, making Halve-Z a flexible theme was a slightly different challenge. I had to refactor and combine page layouts to allow proper template overrides, implement security policy mechanics, and then refactor again to get all the new elements and improvements to work seamlessly. Oh, and, of course, glitched bits!

#Workflow

Besides switching SSGs, the host side also received an upgrade. I finally dropped GitHub Pages and moved to Netlify. Now I can actually control HTTP headers (yea!) and branch/pull request deploys are baked in!

netlify.toml:

[build]
publish = "public"
command = "zola build"

[build.environment]
ZOLA_VERSION = "0.18.0"

[context.branch-deploy]
command = "zola build --base-url $DEPLOY_PRIME_URL"

[context.deploy-preview]
command = "zola build --base-url $DEPLOY_PRIME_URL"

[[headers]]
  for = "/*"
  [headers.values]
    Access-Control-Allow-Origin = "*"
    X-Frame-Options = "DENY"
    X-Content-Type-Options = "nosniff"
    X-XSS-Protection = "1; mode=block"
    Cache-Control = "public, max-age=604800, must-revalidate"
    Content-Security-Policy = "frame-ancestors 'none';"
    Referrer-Policy = "strict-origin-when-cross-origin"
    Strict-Transport-Security = "max-age=63072000; includeSubdomains"

Just push commits, then preview changes via prefix. Sweet!

#Submodule

Themes in Zola can be handled as Git submodules, and GitHub's Dependabot can automatically pull updates for users' projects.

.github/dependabot.yml:

version: 2
updates:
  - package-ecosystem: "gitsubmodule"
    directory: "/"

This configuration will raise pull requests when updates are available.

And to install this theme as a submodule:

git submodule add https://github.com/charlesrocket/halve-z themes/halve-z

#Design

The index/homepage now has a scrollable info block, so it could fit more text without breaking the layout, and the main logo is now optional. I also added a footer and some glitching effects to make things more gripping, e.g., when the logo is off. These elements are optional and controlled via config.toml.

#Features

Besides basic functionality like tags and categories, Halve-Z also supports automatic color profiles, extended HTML formatting, a commenting system (Giscus), and media and data shortcodes.

The original layout for the projects page was just a container with an image and a link. But I wanted to make it more informative, so I ended up implementing simple project cards with basic details and a language indicator.

#Security

It took me a while to figure out how to handle CSP at the level I needed. Of course, my current implementation might be overkill, but https://* wildcard for img-src directive feels too reckless, so the theme's front matter has a dedicated csp_img option.

Since I decided to support external media, I had to wrap inlined CSS for images into custom macros to be able to calculate hashes for the style-src directive.

{% if config.extra.csp == true %}
  {% if page.extra.image %}
  {% set page_image_hash = get_hash(literal=macros::image_style(url=page.extra.image), base64=true, sha_type=512) %}
  {% else %}
  {% set default_post_image_hash = get_hash(literal=macros::image_style(url=default_post_image), base64=true, sha_type=512) %}
  {% endif %}
{% set main_images_hash = get_hash(literal=macros::main_images_style(home_url=home_image, posts_url=post_list_image), base64=true, sha_type=512) %}
{% endif %}

It's a bit awkward, but it works. And I got A+ grades on Mozilla's Observatory.

#Templates

Zola is using a different template engine, so I had to adjust the original approach to be able to fully utilize Tera's blocks. Partial template overrides are very handy!

{% block body %}<body id="posts">{% endblock body %}
    <div class="block-left">
      <div class="content">
        {% if current_path | default(value="none") != "/" %}<a href="{{ config.base_url }}" class="logo"><img src="{{ get_url(path=config.extra.logo, cachebust=true) }}", alt="logo" width="64" height="64"></a>{% endif %}
  {% block left %}
        <h1 class="section-title">{%- block page_title %}{% if page.title %}{{ page.title }}{% elif section.title %}{{ section.title }}{% elif term.name %}{{ '#' ~ term.name }}{% else %}{% endif %}{% endblock page_title %}</h1>
  {% endblock left %}

In this example, users can override the body block and assign different IDs to a tag, or set a custom title with the page_title block.

#Conclusion

Somehow I managed to reach the target functionality without completely messing up the CSS. Or I just like to think that way because I really want to rewrite the whole ruleset from the ground up. For now, I will just enjoy the result and wait for Tera v2.