I've just completed a pretty substantial rewrite of my homepage, all while keeping all existing URLs and pages intact as well as possible. This blog post rambles about the history of this page, and its current technology stack.
This page has gone through more technical transitions than justified, but I enjoy experimenting with different technology. I think there has been three major versions so far.
When I started this site in 2013, I had never written a single bit of html in my life, but Python sounded fun and exciting, so I set up Django on my uberspace, and typed away. 99% of the content on this site has always been static, but using Django had allowed me to do fun things like including a different entry from the Meaning of Liff each time the page was refreshed.
The earliest snapshot I've found is from October 2018.
Back at that time, I was still on 25a0.com.
Django is fine and all, but I wanted to simplify my infrastructure, and it has always felt weird to use a full web server for something that could be achieved with a folder of html files.
So in late 2018 I started to rewrite the site. I thought it would be cool to do everything with homemade tools, so I wrote my own static site generator, as well as my own Markdown parser. The Markdown parser was, of course, incomplete, and had only the features I needed at that time. I didn't want to write my own template engine, so all html was generated from Lua code. That looked something like this:
-- recipe title
sink:start_tag("h1")
sink:write(recipe.name)
sink:end_tag("h1")
-- link to source
if recipe.attributes.source then
sink:start_tag("a", {href = recipe.attributes.source,
target = "_blank",
class = "btn btn-outline-dark btn-sm d-inline mx-1 p-1"})
sink:tag("i", {class = "far fa-external-link fa-fw mr-1"})
sink:write("Source")
sink:end_tag("a")
end
-- name of author
if recipe.attributes.author then
sink:start_tag("div", {class = "d-inline mr-2"})
sink:tag("i", {class = "far fa-user fa-fw mr-1"})
sink:write(recipe.attributes.author)
sink:end_tag("div")
end
It was clumbsy. There was boilerplate for every single HTML element, and it was tedious to write pages this way. Lua is great for DSLs in theory, and I'm not sure why I didn't do something more similar to Leafo's DSL approach.
Either way, because it was so much effort to write new pages with this approach, I never changed anything about the site once the rewrite was done.
I grew tired of not being able to quickly add pages to the site, and I had some spare time in my week once my master's thesis was done.
In order to not repeat mistakes of the past, I had set a couple of design goals for this new approach.
All present URLs should still point to the same content after the rewrite. Luckily at that time this site had like 40 pages, so that wasn't too difficult.
Is it a document? Write it in Markdown. Is it structured data? Write it in Lua. Is it HTML or a HTML template? Write it in HTML. This might seem obvious, but given my previous approach, it felt necessary to make this a specific requirement.
My site is a simple one, and essentially I just need three building blocks: A Markdown parser, a template engine, a way to read structured data, and a bit of code that puts everything together.
Markdown Implementing a Markdown parser is interesting, but implementing a complete Markdown parser is not easy. John Gruber's initial blog post that introduced Markdown was more of a description than a syntax, and so over the years different dialects and interpretations of "Markdown" materialized across the Internet.
Only thanks to CommonMark do we now have a proper syntax that clearly defines what a Markdown document is. In my opinion, the biggest achievement of CommonMark is that it defines an unambiguous syntax for Markdown without compromising the feeling of writing natural text.
Luckily, there are Lua bindings for commonmark, by John MacFarlane, who is also on the CommonMark team.
Templates There are lots of approaches for how to do templates. I stumbled upon lustache, a Lua implementation of mustache templates. mustache is advertised as "logic-less templates", which I find a bit of a misnomer, because there are obviously still loops and branches in those templates, but I liked the idea that rendering the data should be a trivial step where not much logic needs to happen. I have a few gripes with lustache, but overall it's working fine for me.
Handling structured data Lua has gotten a lot of praise for being a great language for writing config files, and I find that the same is true for writing any small pieces of structured data by hand. This is the data from which this recipe is rendered:
return {
title = "Lemon mousse",
source = "https://www.chefkoch.de/rezepte/416511132062273/Feine-Zitronencreme.html",
servings = "Serves 4",
publish_date = "2018-11-19",
abstract = "Light and not too sweet summer dessert",
category = "Dessert",
tags = {"dessert", "summer", "cold"},
ingredients = [[
- 3 eggs
- 4 Tbsp sugar
- 3 leaves of gelatine
- 1/8 l heavy cream
- 2 lemons
- 4 Tbsp water
]],
instructions = [[
Soak the gelatine in cold water in a small pot. Separate the eggs, and zest the
lemons with a fine grater. Put a bit of lemon zest aside for decoration.
Beat egg yolks and sugar until fluffy, then add lemon zest and the juice of
both lemons. Heat the gelatine over low-medium heat until it is fully dissolved.
Then stir the gelatine into the egg mixture, and refrigerate for about half an
hour, or until the mixture has fully set.
With a whisk, break up the set mixture. Beat the heavy cream to stiff peaks,
and add it to the mixture with a whisk until homogenous. Finally, beat the
eggwhites and very carefully fold them into the mixture. Decorate with a bit of
lemon zest, and keep refrigerated until served.
]],
}
Loading this data is as simple as local recipe = assert(loadfile("data/recipes/lemon-mousse.lua"))().
Putting the two together
I had considered using a pre-made static-site generator to actually produce the pages from the data, but I wanted to retain control and flexibility over the structure of my site, and it didn't take much code to actually generate pages from the data.
The different sections of my site are quite heterogenous, so I still use a bit of code to generate each of them, but it's a trivial amount compared to my previous approach. For example, this code generates https://25a0.org/localmpgames:
-- 25a0.org/localmpgames/index.html
do
generate({"localmpgames", "index.html"},
{
data = {
extra_css = {"localmpgames"},
games = assert(loadfile("data/localmpgames/games.lua"))(),
data_fname = "localmpgames/games.lua",
},
partials = {
content = readfile("templates/localmpgames/index.html.mustache"),
},
section = sections.local_mp_games,
title = "Local Multiplayer Games",
})
end
Homogenous pages, like recipes or blog posts, are of course discovered automatically. To add a new recipe, I simply add the corresponding Lua file and rebuild the page. The recipe will be rendered automatically, and the index and tag pages will be updated accordingly.
If a page is generated from Markdown or data, I decided to include links back to the actual data at the bottom of the page. For example, at the bottom of this page you'll find a link to the Markdown document from which this blog post was rendered.
0x25a0 is the unicode code point of ■ (black square). I picked it in 2011 as
my Minecraft username because I thought it was a cool choice for a game all
about cubes. Since then it snuck into other things like my twitter
handle, my GitHub
username and of course my domain name. It's a
terrible choice for a domain name, I know.
This page is generated from data and markdown.
■