How I built this site

I wanted to launch a blog for myself. I knew that I wanted to use a static site generator. I also wanted to give Tailwind an honest try.

I looked at Hugo first. It seems to be a very popular choice. We use it at work. I never really cared for it. Hugo is a very sophisticated static site generator, but much more than I need.

The next SSG I looked at was Cobalt. It worked well enough. I had a basic blog up and running in an evening. Unfortunately, Cobalt prove to be a little too inflexible.

I could theme the shell with tailwind's utility classes. However, the rendered Markdown had header tags that I couldn't add classes to. I am sure I could hack some kind of post-processor together, but hacks like that were not appealing.

Today, I watched a live stream on System Crafters called This is the Future of Scheme Hacking. The folks from the Spritely Institute were showing off Hoot and talked about Guile. I kept hearing them mention Haunt.

I looked at Haunt, and it was pretty straight forward. It is very well documented. The source code is very clean and readable.

This is your brain on Lisp

Ever since I was a little programmer, I used to joke, "I'll never try Lisp I'm afraid I'd love it too much.". For over 25 years, I resisted it. Last year, I cracked and gave in.

Despite using Emacs most of my career, I never fully embraced it. However, in January of last year, I stopped resisting Emacs and went all in on learning how to use it properly. I ditched Doom Emacs and rewrote my config from scratch.

The young Eric was right to be afraid. I absolutely love Lisp. The patterns and metapatterns danced. I swam in the purity of quantified conception.

Learning Emacs Lisp prepared me for my next adventure in Lisp. I tried learning Common Lisp, it just wasn't for me. Scheme, on the other hand, sparked something in me. Maybe it was years of Erlang and Haskell that taught me to love pattern matching, I don't know.

After I watched the live stream, I read the excellent scheme primer that Christine Lemmer-Webber wrote. I now knew enough Scheme to be dangerous. I was now prepared to dive feet first into Haunt and Guile. I slapped together a flake.nix devShell, and got to work.

The Haunt manual has an excellent tutorial on Haunt. That tutorial will do a much better job teaching you Haunt than a couple of puny paragraphs here will do. So I will tell you about the part of the experience that I really enjoyed: Writing XML in Scheme.

You might be asking yourself, they enjoyed writing XML? Who enjoys writing XML? Well, let me tell you, writing XML in Scheme is a joy.

SXML is a joy

About 12 years ago, I had a harebrained idea to write an HTML template engine for front-end development using arrays. It looked like this:

It even supported dynamic updates to the DOM after the root element is generated:

Little did I know, that great minds think alike, and Guile has an XML syntax called SXML. SXML is XML encoded as S-expressions.

The chunk of HTML:

Looks like this in SXML:

You might say that it is hard to read. I thought so too at first, but over time I found it no harder to visually parse than HTML.

SXML is just data. That means that it is easy to generate chunks of it using quasiquotation expressions. A quasiquotation is a way to generate a Lisp value by mixing s-exp literals with Lisp expressions.

Here's a piece of code that generates an unordered list using the map function.

The back quote starts the quasiquotation, the comma tells Guile to evaluate the next form, and replace it with the evaluated value. This quasiquotation evaluates to this value:

Using quasiquotation and functions to build up a tree of SXML is very composable. A template language like Go's html/template requires me to learn an entirely new language to generate HTML. With a quasiquotation, I can generate XML with Scheme code.

Given how joyful SXML is in Scheme, I'm actually excited to see what Guile has available for generating YAML and JSON in this way.

As another example, here is what the content of my homepage currently looks like

Rewriting SXML

Remember when I said that I had to give up on Cobalt because I couldn't skin the Markdown? Well, I had the same problem with Haunt. The SXML that Haunt generates from the Markdown lacks any styles.

Fortunately, Haunt has the concept of a reader which handles the parsing of files. A Reader is a function that returns two values, a metadata alist and the converted SXML.

What I did was create a new reader based on the built-in Markdown reader. My custom reader uses the powerful SXML matching and transformation functions to add Tailwind utility classes to the tags that need them.

The patterns resemble the SXML expressions I'm writing, so I found that the SXML pattern matching and transformation functions are much more ergonomic than DOM hacking or gasp writing XSLT.


The sxml module provides a function called sxml-match. sxml-match will use pattern matching to rewrite the tag given to it.

Here is a simplified version of the sxml-match call that I use to rewrite the tags the Markdown reader produces.

So given this SXML tag

The first rule matches the a in the head of the SXML.

The . ,attrs part of the pattern tells sxml-match to collect all the attributes into a list called attrs

Likewise, the . ,body collects the child nodes of the <a> tag into a list called body.

After pattern matching, we have the following variables

The second part of the rule is a quasiquotation to reconstruct a new SXML tag using the values we extracted using the pattern

The sxml-match documentation goes into great detail on the pattern syntax. For the most part, the pattern matching follows the conventions of match.

What is nice about sxml-match is the pattern can be arbitrarily deep. You can see an example of that where I modify the <pre><code><code></pre> tags.


The sxml-match function is only one part of the puzzle. It only matches on a single tag. I need to walk the entire SXML tree and rewrite any <a> tag, <p> tag, or <pre><code> tag.

The sxml transform module provides a function called pre-post-order that does just that. It matches on tag and calls a function like my style-tags function. The matched tag is replaced with what is returned by the handler.


Guile is what I wish Python was. It is a simple, extensible, expression based language without a GIL. The documentation is incredible, and it has a lot of batteries included. Guile code is also quite fun to hack on.

Overall, using Haunt was a very pleasant experience. David Thompson and friends did a great job on it. I would recommend it to anyone looking for a static site generator.

Related Links