Notepad

Racket and Punct: Markdown with Metadata

Written by Dominik Pantůček on 2024-07-18

In 2016 we decided to implement our website in WordPress which was deemed adequate at that time. However the times are a-changing and now we have migrated to our custom publishing pipeline using Racket and Punct.


Our original decision to use WordPress was motivated by supposed ease-of-use, multilingual pages and long-term security support. However it turned out that maintaining such site with all the plugins over the years requires from time to time some rather unpredictable updates to our custom theme as well.

In order to improve the situation we considered multiple options and converged on custom static web pages generator where the content is a Markdown text of the CommonMark flavor and the new design - which we also developed during the process - is plain HTML5 with CSS3.

As a part of the new design development we managed to meet all the original goals we wanted with the previous website iteration. On top of that the new presentation supports desktop dark mode with the feature to enable or disable it by the user. Thanks to CSS variables this was possible even without using any client-side Javascript.

For loading the CommonMark documents the Punct Racket package is used. Punct's addition is the meta-data block at the top of the document which is a simple dictionary of key-value pairs. These keys are then used to select appropriate HTML template for given page when it is rendered, to sort the menu items and to place blog posts properly in the web presentation URL tree based on its published attribute.

For example the "About Us" CommonMark source for this website contains:

#lang punct
---
title: About Us
order: 20
class: about
---

About Us
========

...

The first line tells the racket module loader to use punct language for loading the document file as module. After it we can see the meta-data block and generic CommonMark text follow.

Loading a punct document is pretty straightforward. As it compiles as Racket module containing the doc binding, you just need to require the module and extract the binding. A typical usage leverages the dynamic-require procedure call.

#lang racket/base
(require punct)
(define page-doc (dynamic-require "about.page.en.rkt" 'doc))

The filename convention we opted for allows us to have the same page available in multiple languages - a feature we use for the main web presentation.

Punct renders the documents as single article element and therefore it is pretty straightforward to add a custom CSS class to alter its visual presentation in the final rendered web page.

The web presentation hierarchy is just a directories hierarchy that translates directly to hierarchy of URLs. The only exception is the blog which is generated based on published key.

The whole publishing process is then realized as single GitLab CI pipeline that generates the resulting hierarchy full of static HTML files and copies it to the web server using rsync. Each commit to the master branch can be immediately seen on the development web server and when all the changes are ready for publishing the release is tagged using a simple git tag and after the pipeline runs the new release goes live.

Perhaps in the future we will publish a post about building the website tree from the filesystem hierarchy and meta-data.

As always, see you next time!