I like software built with care , healthy teams and puzzle pieces that fit
Admitting to building your own static site generator is a bit like admitting you enjoy pressure-washing videos. People look at you with a mix of mild fascination and concern, wondering why you aren't doing something more productive. Yet here we are. In my previous post, I defended the philosophy of technical self-reliance. Now, let’s talk about how this beast actually functions.
Most modern static site generators are built on a premise I find fundamentally flawed: they exist to translate text-based markup languages (usually Markdown) into HTML and CSS. If you're a non-technical writer, this is a godsend. But if you already speak fluent HTML and CSS, Markdown is a leaky, restrictive abstraction.
I don't want to write Markdown. I want direct, programmatic access to the full expressiveness of the web's native formats. I need a tool that lets me manage the complexity of code re-use through simple composition, without burying me in a pile of YAML front-matter and nested templating engines.
To build this, I reached for Clojure. Clojure is uniquely suited for building domain-specific languages because of Lisp’s defining superpower: homoiconicity (the code is represented as data structures that the language itself can manipulate).
Instead of parsing string-based HTML templates, we can represent HTML elements and inline CSS directly as native Clojure vectors and maps. The Clojure ecosystem already has a brilliant library for this called Hiccup. Here is what Hiccup looks like in action:
user=> (html [:span {:class "foo"} "bar"])
"<span class=\"foo\">bar</span>"
Notice how clean that is. There are no trailing slash errors, no closing tag syntax, and no string interpolation bugs. It is just structured data.
Once your markup is represented as data, the problem of templating resolves into a problem of data composition.
To manage our site's assets and configuration, I pulled in Aero, a small Clojure library for parsing configuration files (written in EDN, Clojure's data format). Aero is excellent because it allows you to define custom tag literals. In Lisp terms, think of tag literals as compiler hooks that evaluate data structures at read-time.
By using Aero, we can write our entire website's layout and pages in static .edn files, version them with Git, and guarantee absolute idempotency. The compiler reads the data, evaluates our custom tags, renders the Hiccup to HTML/CSS, and writes it to the disk.
Let’s look at the asset configuration for this website's home page:
{
:type :html
:slug "/index.html"
:content #template ["pages/index.edn" [:content] {:path "/"}]
}
The heart of our composition engine is the custom #template tag literal. When Aero's reader encounters #template, it treats it like a function call. It takes three arguments:
"pages/index.edn": The path to the template definition.[:content]: A pull vector (similar to Clojure's get-in) to extract the exact portion of the evaluated data we want.{:path "/"}: A map of input variables passed into the template's rendering context.Here is a look inside pages/index.edn:
{
:color #include "../styles/color.edn"
:content
#template ["../components/layout.edn"
[:content]
{:title "home"
:body #ref [:body]}]
:body
[:div
{:style
#css {:display :flex
:flex-direction :column
:justify-content :flex-start
:align-items :flex-start}}
[:div
{:style
#css {:font-size "50px"
:font-weight 700
:color #ref [:color :yellow]}}
"Hi. I'm Adam Tait."]]
}
Notice how we compose the layout. The :content key calls the layout template, passing the :body of our home page as an input variable via the #ref literal. The #css tag translates Clojure maps into inline style strings. It is simple, explicit, and lacks any magical "black box" behavior.
In his famous talk, Are We There Yet?, Rich Hickey makes a crucial distinction between "simple" (unentangled) and "easy" (near at hand).
Most modern web frameworks are easy—you run npx create-next-app and you instantly have a working site. But they are not simple. They bring along a staggering amount of incidental complexity—Webpack configurations, Babel steps, node_modules folders the size of small planets—that you must eventually debug.
The mathematician Alfred North Whitehead once wrote:
Our custom EDN/Hiccup approach does not hide complexity; it lays it bare. There is no magic under the hood. You have direct access to the raw building blocks of the web, structured as immutable data."Seek simplicity, and distrust it."
Is this a poor abstraction? For many, yes. It requires you to write Lisp syntax to build a webpage. But by building a tool that builds the generator, I created a system I deeply understand. When a bug occurs, I don't file a GitHub issue; I just fix my code.
Published: 2020-11-29