💻 slembcke.net

2024-06-12

My Static Site Generator

So I've had this URL since the late 2000s, but have never really done much with it. I used to be into Ruby and had an Instiki wiki set up here. Unfortunately it hasn't worked for years, and I haven't really cared to fix it. (I'll probably try importing the old content though now that I have somewhere to put it) Instead, for a few years I've been using slembcke.github.io for a blog. The GitHub Pages promise seemed nice. Install Jekyll, write some markdown, and we'll make the complexity of maintaining a site go away.

Unfortunately, that was never my experience. My blog with very default-ish settings, using the most boring of the themes, had 89(!!) dependencies you had to install to make it work. There's even a tool (Bundler), that's supposed to make the pain of managing dependency versions go away. Unfortunately, in reality it just breaks every few months anyway, and it really saps my desire to write anything.

I'm also very annoyed with Microsoft for deciding that it's okay to feed everything on GitHub into an AI model. I haven't decided if I'm going to pull down my existing projects, but I'm certainly not putting anything new on GitHub ever again.

So I made my own...

So I decided I'd had enough. I thought the whole point of static site generators was supposed to be simplicity. All I really wanted was something that would deal with the "web stuff" I didn't care for, and to paste headers and footers onto every page. People seem to like Hugo well enough as an SSG, but it also has a lot of dependencies, and I don't really trust it will work 10 or 20 years from now. After thinking about it for a while I decided on using boring HTML/CSS, Lua, and Make.

I love the simplicity of Markdown, but my experience with it on slembcke.github.io was that it was often as annoying as helpful. The moment you want to do something as trivial as center an image, you still need to know how the underlying HTML and CSS works anyway. I don't really like web languages, but Markdown wasn't really saving me from them either. It was just adding an extra layer on top. Ultimately I decided that Markdown wasn't a must-have feature.

As for Lua, and Make?! I wanted "boring" and ubiquitous tools that would work anywhere. I'd be willing to bet I could easily build my site on a 20+ year old machine. Similarly I'll bet that in 20 years I'll still be able to build it with no modification. That sounds... nice!

Dozens of lines of Lua

Lua isn't my favorite language, but I do really appreciate how it can keep simple tasks simple. ~40 lines of Lua later, I had a template engine to run stuff like the following snippet. That's enough to insert headers/footers, iterate blog posts, format things, etc. A couple dozen more lines and I had a page generator script complete with step debugging. \o/

Let's sing a song!
{{for i = count, 1, -1 do}}
{{= i}} bottles of beer on the wall.
{{= i}} bottles of beer.
Take one down pass it around,
{{= i - 1}} bottles of beer on the wall.
{{end}}
That was fun!

After prototyping out some pages using my templates, and using readable.css to provide the basic style I was going for, I decided to go all in. So far, writting "my own thing" has proved to be very little work. Less than using Jekyll even. (shrug)

Dependency wise, using Lua feels great too. You can compile it from scratch in about a second, so I just froze the version I'm using into the project. Now my only hard dependencies are Make and a C compiler. Since Lua targets C89, you really can build it for nearly any platform using any compiler from the last 30 years.

Make is the best build system... except for C projects

Most people's experience with Makefiles is "that awful 10,000 line file made by running ./configure". Those makefiles (kind of...) exist for a reason, but I will show them no love. Generally, the syntax you need to know from Make is quite simple. For example:

index.html: index.lua lua/generate.lua
	lua lua/generate.lua index.lua index.html

In other words, index.html depends on index.lua and lua/generate.lua (my page generator script). If Either of those dependencies change, then run the command on the next line to rebuild index.html. That's all well and good, but you don't really want to type that out every time you add a page. Instead you'd just use a pattern rule instead like this:

%.html: %.lua lua/generate.lua
	lua lua/generate.lua $< $@

Now you have a rule for any .html file that depends on a .lua file of the same name (that's what the % means), and the page generator script. The dollar sign things are automatic variables, and look like some scary Perl artifact. Really there's just a few of them I bother to remember though:

That's basically all the Make syntax I remember off the top of my head because it's usually enough.

Finally, you just need a list of all the pages you want to generate. You can use a file glob to list them all using a naming convention, but I generally find an explicit list is better than magic. Make runs the first rule in the file as the default, so the convention is to just name the first rule default and make it depend on all the files you want. With that out of the way, everything should get built when you run make. Easy!

default: index.html blog/index.html about/index.html

I will note that my experience using Make for C/C++ based projects is painful though. It doesn't really "understand" C code or the platforms it runs on, leaving a lot of the work up to you. It's why the monstrosity that is Autotools exists (those ./configure files), and that makes things even worse IMO... Still, don't rule out Make for simple build tasks!

Magic Local Server

While you can (sort of) just open html files from disk, realistically you need a server to make CSS and JS work. Maybe the easiest, most widespread option is included with Python. If all you need is to serve up local files, then all you need to do is run this command:

python3 -m http.server

I used that initially, but it did mean that I had to run make, and then refresh the page. It sure would be nice if Python just invoke the build for me on every request. I looked into it and it was just another 20 lines of code to do it. Easy!

import os
import http.server
import socketserver

class ReloadHandler(http.server.SimpleHTTPRequestHandler):
  def do_GET(self):
    if self.path.endswith("/") and os.system(f"make -j .{self.path}index.html") != 0:
      super().send_error(500, f"Failed to build {self.path}index.html")
    else:
      super().do_GET()

ADDR, PORT = "localhost", 8080
with socketserver.TCPServer((ADDR, PORT), ReloadHandler) as httpd:
  print(f"Serving as http://{ADDR}:{PORT}")
  try: httpd.serve_forever()
  except KeyboardInterrupt: pass
  finally:
    print("shutting down")
    httpd.server_close()

Closing Thoughts

So I'm pretty happy with the result. It's simple, fast, and seems like it should be pretty sturdy. It doesn't do Markdown, and it doesn't automatically highlight syntax in my code blocks, but it seems to do everything else I want it to. Unlike Jekyll, I actually understand how it works because it's all just simple page templates. If I want to add a section for my NES games, I just write another page. Surely the process isn't too different in Jekyll, but last time I tried I lost interest due to documentation fatigue. Tools are supposed to make work easier, but if your task is easy enough maybe a powerful tool doesn't make sense.

Anyway, for what amounted to a weekend project, hopefully this site will keep working for a long time to come. :)