James Kalyan — August 02, 2026
Haunt is a static site generator written in GNU Guile. But how does it differ from other SSGs? Well I've tried a few before including my previous favourite: Hugo; I quite liked my workflow using Hugo (GNU Emacs' org-mode + org-hugo + one button publishing to Gitlab Pages) but it just wasn't as flexible as Haunt is.
Haunt is flexible because, being written entirely in Guile Scheme, it's incredibly easy to extend and bend to just the right shape for your site. It is fairly minimal with sane defaults like any good SSG, but it will never force you to compromise when you want something to work a little differently.
Okay that's all talk, let me give you two examples of how Haunt is nice due to its Guile Scheme makeup.
The trick here is that Haunt doesn't have a DSL for writing templates, due to the homoiconicity of Scheme (and Lisp languages in general), we can elegantly use it to create templates AND we get the bonus of having a Turing-complete language at our fingertips during file processing.
To make sense of that, let's look at an example from David Thompson, the author of Haunt:
(use-modules (srfi srfi-41)
(haunt utils))
(define fib
(stream-cons 0 (stream-cons 1 (stream-map + fib (stream-cdr fib)))))
(define count 20)
`((title . "Hello, world!")
(date . ,(string->date* "2015-04-10 23:00"))
(tags "foo" "bar")
(summary . "Just a test")
(content
((h2 "What is this thing?")
(p "This is Haunt. A static site generator for GNU Guile.")
(p "SXML is cool because you can evaluate Scheme code in your blog
posts. Here are the first "
,count
" fibonacci numbers, computed with SRFI-41!")
(pre ,(object->string
(stream->list
(stream-take count fib))))
(p "Guile Scheme is great, eh?")
(img (@ (src "/images/guile-banner.small.png"))))))
Above is a Scheme file that returns a simple S-expression in the SXML format which represents an HTML page. This file is processed by Haunt in a completely configurable way to produce the output HTML that you will eventually serve. The SXML is created using a backtick/grave to turn the following expression into data (called quoting), but inserting the results of the expressions that are prefixed by a comma (called unquoting). This is extremely natural for any Lisper and David is using it here to compute count number of Fibonacci numbers using the function fib which he defined at the top. It's all Scheme and it's all awesome.
There are also a number of other ways to write pages, such as Skribe and Markdown which are more natural for simpler prose pages (this page is written using Markdown for example). You can mix and match as needed and even add your own "readers" if you want another format that is not yet supported. But I'll leave you to RTFM on that front.
All of the mechanisms Haunt uses to perform site generation are also written in Scheme and designed to be augmented and tinkered with. There's no hard boundary between your posts and your site generation code. It follows that same sort of mindset as something like GNU Emacs where you're really meant to make it yours at the end of the day, and I personally like that!
Okay I haven't gotten to finishing my CSS builder for Haunt yet, but that's probably good so I can show off something simpler.
For my residential electrical service calculator tool I used SXML to generate the page because I needed finer control over the resulting HTML. But structurally, HTML (and hence, the raw equivalent in SXML) can be quite verbose. Avoiding typing out all that mess is kinda the point of a site generator after all. So anyway, for this tool I needed a way to make HTML <label>s with <input>s inside that had some default attributes based on the input type. I also needed to add a <span> with a custom CSS class if the input type was a checkbox. This is all highly particular to this one page so I can just define a constructor for this pattern with some parameters that I can adjust where called:
(define* (labelled-input label-text #:key id type placeholder (value 0) (min 0) (oninput "calculateService()"))
`(label (@ (style "display:flex;flex-direction:row;justify-content:space-between;gap:1rem;")
,@(cond
((string= type "checkbox") '((class "checkbox")))
(else '()))
(for ,id))
,label-text
(input (@ (id ,id)
(type ,type)
,@(if (string= type "number")
`((min ,min))
'())
,@(if placeholder
`((placeholder ,placeholder))
'())
,@(if (and (string= type "number") (not placeholder) value)
`((value ,value))
'())
(oninput ,oninput)))
,@(if (string= type "checkbox")
'((span (@ (class "checkmark"))))
'())))
With this function defined I can now insert labelled inputs within the SXML like so:
`((title . "Residential Service Calculator")
(author . "James Kalyan")
(content
...
(div (@ (style "display:flex;flex-direction:column;gap:.5rem;"))
=====> (labelled-input "Is the AC interlocked with the electric space heating?"
=====> #:id "interlocked-ac-and-heating"
=====> #:type "checkbox")
...)))
That is easier to read and easier to maintain for me. Aside: I'm especially fond of these bits of inline CSS that aren't worth putting in a CSS file but are also too cumbersome to copy to all the other <label>s on the page or whatever; abstracting it file-locally with Scheme feels so right.
The ease with which I can write these little helpers is one of the reasons Haunt is so appealing to me. I'd suggest trying it out if your in the market for a new SSG!
Just a few things to note to keep myself grounded here...
Special thanks to David Thompson for Haunt and the other great projects he has published over the years.