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.
sxml-match
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.
pre-post-order
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.
Conclusion
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.