splitbrain.org

electronic brain surgery since 2001

Hugo vs. Lektor (vs. Grav)

About two or three years ago I completely revamped the web site of my employer CosmoCode. Back then I picked Grav as the CMS.

Grav

Grav is a PHP based flat file content management system. I had made a smaller website with it for Kaddi's mum's house boat rental business so I knew my basic way around in it.

Using Grav feels very much like using a static site generator. Everything is built based on a folder structure with markdown files. But stuff is not built statically but delivered through PHP (much like DokuWiki does it). Consequently, Grav also comes with an admin interface plugin that allows you to create new content in a traditional CMS way.

However when building my company web site it became apparent that the performance of the resulting site was acceptable only when the caches were hot, but on every deploy response times sucked.

My solution back then was to turn the semi-dynamic site created by Grav into a fully static HTML dump using a self-written web crawler. It works, the website is fast and robust. End of story.

Except that I was never really happy with that somewhat hacky solution. The deploy process is quite slow: build a Docker image, set up Grav and the crawler, wait for the crawler and finally rsync the result to the server. It takes about 10 minutes for a full deploy, even though the web site isn't massive.

In addition, working with Grav in development mode, where no caches are active, is somewhat sluggish. Also really no one ever used Grav's admin interface anyway. We're more comfortable with editing the markdown files in our favorite IDE and deploy by git push.

Static Site Generators

So the last couple of days I looked into static site generators (SSG) to see how easy it would be to move our existing content over.

There are a couple of things that are special about our website that I need to have covered.

Most importantly is “content blocks”. When you look at a page like this (pictured on the right), you can see that a typical page consists of several sections. Each section can have different colors, a background image, a skew to the left or right and some other properties that influence how the content in it is rendered.

In Grav this is achieved by something they call modular pages. Instead of having one markdown file per page, a page can consist of multiple markdown files, with each using their own Twig template.

This is rare in static site generators, but there are ways to achieve similar results using different approaches.

Another feature we make extensive use of, is the ability to use Material Design Icons everywhere. In Grav it was super simple for me to add a little bit of PHP to simply embed an icon just by name. Either in the template or in the markdown itself using a shortcode.

Finally we need multilingual support for German and English, including pages where only one of two languages is available. And we need a way to automatically list certain pages in the footer (to build the pseudo “hashtag” list).

What our site does not have is a traditional blog. Though we use taxonomies for automatic reference listings, which is similar.

Hugo

With these things in mind I started look into Hugo. Hugo is one of the most popular SSGs nowadays and is mentioned a lot. So it was an obvious choice.

Thanks to the go language, Hugo is a single binary which makes “installing” it a breeze. As promised it is super fast and it's server mode for local development even hot-reloads your browser for you whenever you make changes to the content.

I really wanted to love Hugo. Using a well known and super fast tool would be great. Unfortunately I struggled a lot!

Hugo has extensive documentation, but I found it really hard to grasp many of Hugos' core concepts and terminology. Much of it felt unnecessary convoluted. Much like they started with a very specific use case in mind and broadened it more and more over time without ever stopping to look if those initial ideas still made sense.

For example, I do not fully understand their obsession in differentiating between branch and leaf bundles or how “Kinds” differ from “Sections” and why it is even necessary to have a concept of sections instead of simply understanding the content as a tree of nested pages.

I wonder if DokuWiki's concepts feel similar weird to newcomers.

For implementing content blocks, I used their short code mechanism. Basically you can define a template with an inner placeholder and in your markdown simply wrap that template around your content and pass optional parameters. So a section shortcode in use might look like this:

{{% section skew="left" image="background.jpg" %}}
# Markdown Headline

markdown goes here
{{% /section %}}

However, the go templating engine sucks in comparison to Twig. There seems to be no easy way to append strings. There are weird workarounds like using a “scratch pad” or using print statements to redefine variables. Stuff like this is normal in go templates:

{{ .Scratch.Set "class" "" }}

{{ $type := or (.Get "type") "default" }}
{{ .Scratch.Add "class" (print "tpl-" $type " ") }}

{{ $skew := .Get "skew" }}
{{ if .Get "skew" }}
    {{ .Scratch.Add "class" (print "skew-" (.Get "skew") " ") }}
{{ end }}

...

<section class="{{ .Scratch.Get "class" }}">

Even worse, there is no easy way to inherit and extend templates. Hugo by default makes all templates inherit from a baseof.html template which is fine, but if you want more inheritance you're screwed. Eg. for shortcode templates there's no inheritance at all.

For example, in our site we have some sections that require a bit more formatting than just having a different CSS class. In Grav they inherit from the base section to handle all the standard color, image and class setup and then wrap their own HTML around the contents. Not easily done in Hugo's templating.

Then there's the weird things that do neither throw an error nor work as expected. I mentioned their differentiating between leaf and branch bundles - the practical difference lies mostly in if you use an _index.md (notice the underscore) or an index.md for your content.

I used the wrong one at one place and my code for automatically listing files in the footer behaved super erratically. Not just doesn't work, or throw an error, but work sometimes or only in part or only on some pages or only if you changed a page but not if you restarted… Luckily I got help in their forums.

The advantage of Hugo being popular, is that there are plugins that already do what you need. For example the icon functionality is provided by a plugin. A bit unfortunate is that for being able to use plugins, you need a full installation of go and are no longer using a single binary.

Overall, I think I could make the website work in Hugo. But each and every step feels like a struggle. It did not feel fun to create content, like it did with Grav.

Lektor

Frustrated by Hugo I looked through the huge list of static site generators and found Lektor.

Lektor is written in Python and caught my eye because Armin Ronacher of Flask fame created it.

What sets Lektor apart from many other SSGs, is that is has support for content blocks right off the bat. It's called Flow in Lektor and similar to Grav, each Flow block maps to it's own Template.

Another feature of Lektor I really liked is that it not only comes with a local development server which recompiles your content automatically but that this server also provides an editing interface. This makes it really easy for non-technical users to create and edit pages:

Lektor's documentation is sparse in comparison to Hugo or Grav and there isn't a huge community to support it. I did get an answer in their Gitter chat though (even if it didn't help at the end).

There are a few plugins available, but for the icon support I had to write my own, which turned out super simple:

packages/icon/lektor_icon.py
# -*- coding: utf-8 -*-
import os
import re
import requests
from lektor.pluginsystem import Plugin
from markupsafe import Markup
 
 
class IconPlugin(Plugin):
    name = 'Icon Plugin'
    description = u'Embed SVG icons in your templates.'
 
    repos = {
        'mdi': 'https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/%s.svg',
        'fab': 'https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/svgs/brands/%s.svg',
        'fas': 'https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/svgs/solid/%s.svg',
        'fa': 'https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/svgs/regular/%s.svg',
        'far': 'https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/svgs/regular/%s.svg',
        'twbs': 'https://raw.githubusercontent.com/twbs/icons/main/icons/%s.svg',
    }
 
    def on_setup_env(self, **extra):
        self.env.jinja_env.globals.update(
            icon=self.icon,
            inline_svg=self.inline_svg
        )
 
    def icon(self, name, repo='mdi'):
        path = self.get_config().get('path', 'assets/icons/')
        full_path = os.path.join(path, repo, name + '.svg')
 
        if (os.path.exists(full_path)):
            return self.inline_svg(full_path)
 
        # download and save icon
        down_path = self.repos[repo] % name
        response = requests.get(down_path)
        if response.status_code == 200:
            os.makedirs(os.path.dirname(full_path), exist_ok=True)
 
            # fix attributes and add class to svg
            svg = response.text
            svg = re.sub('<!--.*?-->', '', svg)
            svg = re.sub('(height|width|xmlns:xlink|id)=".*?"', '', svg)
            svg = re.sub('<svg ', '<svg class="icon" ', svg)
 
            with open(full_path, 'w') as f:
                f.write(svg)
        else:
            raise Exception('Icon not found: %s' % down_path)
 
        return self.inline_svg(full_path)
 
    def inline_svg(self, full_path):
        with open(full_path, 'r') as f:
            return Markup(f.read())

Lektor uses the Jinja2 templating engine which inspired the PHP Twig engine (or was it the other way round?) so I felt right at home there. Proper inheritance and available functions make template development a breeze.

Overall, Lektor is somewhat bare bones and expects you to create what you need. For example there's no automatic sitemap generator. You need to code one yourself using a template. On the other hand I felt much more in control.

However there's one thing that really bothers me. As mentioned above, at CosmoCode we prefer to edit the markdown files rather than using an admin interface.

Lektor uses it's own format to save content pages. And that format is ugly as hell. Here's a very simple example using one flow block:

title: Welcome to cchp!
---
body:

#### section ####
body:

# A section
first section
----
skew: right
----
color: #0000ff
----
background: #00dd00
----
accent: #ff0000

Yeah, instead of using a traditional “frontmatter followed by markdown” approach, Lektor thinks in models and fields. The model defines which fields make up a page (or a flow block). The resulting *.lr pages are no fun to edit by hand which defeats the purpose of an SSG to me.

I was really sad when I discovered this. Lektor felt like a perfect fit until the ugly data format really soured it to me.

What next?

I spent about two days on Hugo and Lektor. But neither could really convince me to make the switch from Grav. So for now I will probably continue to just use what works.

There are more than 300 SSGs listed in this table of site generators. I took a quick look at Cecil, but it seems their shortcodes can't wrap around markdown content and I don't see any other way to support content blocks. I wouldn't mind using a JavaScript based one, but they all seem to focus on pushing out JavaScript to the frontend which I don't want.

Maybe I will try another SSG some day. Maybe I will cave and do write my own. But for now, I am back to where I was. A somewhat hacky but working solution.

Tags:
hugo, grav, lektor, ssg, cms, review, webdev, software
Similar posts: