slippery nickels

static site generator

tl;dr it's written with lisp and runs on babashka using selmer and hiccup.


I didn't want to pay much to host this website, so a static site generator was pretty much a given. I'd never really used a static site generator for real before, though, just tinkered. I spent some time looking around and just couldn't really settle on any of the existing offerings. They're all great, I was just very unmotivated to learn any of them deeply enough to make them do what I wanted.

So as is true for all programmer mid-life crises, Lisp comes to the rescue.

Lisp is Lisp, mostly, sorta, but I only really know Clojure. One thing I knew I didn't want is a bunch of extra stuff like node_modules/ and multiple project files and all that. That meant no ShadowCLJS. I also didn't want to use the JRE, so Clojure itself was not ideal. The choice was clear, and so I went with Babashka.

But why?

Because I wanted to?

Originally, the website used Javascript to load pages into the main window, and if I had stuck with that, I'd now be using HTMX to do it. Specifically, that hx-boost feature, that thing is neato. Have you seen it? Fuckin' neato, dude.

But I use Lynx on my workbench laptop because I'm slightly cooler than your dad, and sites that rely heavily on Javascript just suck shit to use in Lynx. You can't even browse Github with it. Ridiculous. I wasn't going to have that, and it only took a minute or two to realize I didn't need Javascript at all. Not even a little.

Maybe a little, eventually. As a treat. (And for interactive things.) Also, hosting a bunch of text files is, like, really cheap.

I started looking through static site generators and frankly hated all of them. It felt like mostly a waste of time to try and learn one well enough to bend it how I needed it to bend, or to not be bent by it the way so many want to bend you. Bend. So I just figured I'd write my own.

This is the part where people usually say something funny like, "that might have been a mistake" but fuck that, it was a great idea. Writing stuff like this with Lisp is fucking awesome, you should totally do it.

Basic Generation Process beep boop

Each major section of my site has its own custom generator. I wrote a macro for defining new ones cleanly because it felt cool to do it that way. Each one is exposed as a command using a Babashka project config, so I can run any or all of them however I need. But I usually just run them all every time.

Each generator has a define set of input pages and a directory of static files if needed, and from there it's exactly what you think it is: the templates are enumerated and processed, spit out into a directory, and then the static files are copied. What else would it be, c'mon.

Fancy Stuff

Really, most of the fun is in the custom tags. That sidebar nav over there uses a couple to build the menus, and my wishlist and manga collection pages make heavy use of them. Those would be incredibly tedious to manage otherwise.

Syntax highlighting for code is the one silly piece of this whole thing. I didn't want to write my own syntax highlighter, and there wasn't really anything readily available for Clojure that I could use in Babashka (that I could find, anyway). So I just used Deno and Shikiji, since the former lets me just have a bare Typescript file without all that package.json and node_modules/ nonsense, and the latter has an output mode intended for Markdown. Ergo, statically colored code.

So I just threw that together and wrote a function to invoke it and feed it code through stdin. Works like a charm, and I can even use any VS Code syntax theme that I want.


	(defn highlight-code
	  "Executes a Deno utility to output statically-highlighted code in HTML."
	  [lang code]
	  (let [deno (or (fs/which "deno") "/root/.deno/bin/deno")
	        cmd [deno "run" "tools/lighter.ts"
	             "--lang" (name lang)
	             "--theme" (:code-theme c/config)]
	        proc (p/process {:in code :out :string :cmd cmd})]
	    (:out (deref proc))))
	

Anyway

That's about it. I just wanted to say something about it. You can check it out here if you want.

I dunno, something seems off...

But wait! There's more!

Fast forward several months, I'm fairly unhappy with how messy the codebase has become, and decided I needed to rewrite it. So I spent an entire day in the typical programmer's ups and downs and banged out a pretty good framework for a new version.

Now namespaced under generator, the old codebase got slid to the side to make way to the new generator2 namespace. My initial focus was a nice, reliable tree of files to be processed, and site-based configs. The latter was simple enough and boring, I'll skip talking about it.

I threw together a simple spec for resources and got to work sticking useful metadata on them. Namely, whether they fit into one of several types: templates, images, scripts, etc. Babashka's glob function was fine for this.

I added inferred subtypes to these as well so I could later easily implement build steps for things like JS/CSS minifiers, SCSS/Less, and whatever else. They just check extensions, but I figure people can be responsible enough to not be silly.

There was a problem, though. I didn't want my custom tags in the generator's source tree, they're specific to my website. Luckily, adding custom tags to Selmer is an atomic operation, so using load-file on a vector of Clojure source files Just Works(TM). You can even load a namespaced prelude that your tag definitions can import. Just make sure it's first in the list.

Oh yeah, you can import stuff from the generator's codebase as well.

This generator has been my first big Lisp project in a while, but after the first version, I'd got a few of my chops back. Multimethods made mincemeat out of the process of processing resources resourcefully. As if by serindipity, Clojure multimethods can be dispatched based on the type metadata I added to resources. So... that ended up taking, I unno, 2 minutes?

🞀

slippery nickels

🞂