Building a single-file static site generator

Site generators are a fairly popular way to spin up a new blog, especially for developers. So when I decided to start this blog, they felt like the easiest route to go with.

However, when I started looking deeper into the options, I was immediately overwhelmed with the sheer number of choices. Besides, there was what I would consider an outrageous amount of boilerplate involved for something that is practically just spitting html files. Zola felt like an exception, but it had an entire templating system learn. All of this led to me deciding to write my own static site generator.

Meet HTMLRewriter (and bun)

In a completely unrelated incident, I was trying out bun, a project that replaces like 10 different things. It is a package manager, bundler, test runner, and has a bunch of useful libraries, including one HTMLRewriter.

It is a native implementation of Cloudflare's HTMLRewriter for its workers, and was a god-send for this project. It is (usually) used to modify html responses before sending them off to the client, and thus can parse and modify html using a neat event-based API.

This was available as a global class in all bun scripts. I'm slightly puzzled by why such specific API is global, and not hidden behind a bun:html import or something, but that's just a nitpick.

Obviously, it was here that I decided to set a completely unnecessary goal to write the entire generator in a single file.

HTML Everywhere

Soon after deciding to use HTML for in all the templates, I was faced with the question of storing content. Sure I can go the tried-and-tested route of markdown files, but again, my brain had the idea to use html for this too.

Unlike other questionable decisions I made while building this, this was a great choice. HTML has a ton of sematic tags that would barely complicate the markup, and I could add complex features like interactive content, by just writing good ol' html.

The tech stack is looking great.

How it works

Content structure

The posts are stored in the /posts directory, and have a simple html structure:

That's it. The c-post wrapper can take additional parameters like a slug and (in future) other metadata. Besides that, it's just the html you know and love.

In the build file, it is parsed by the PostParser class. It's a class and has some boilerplate, but it all boils down to this:

which I think we can all agree, is very simple for something that requires installing no modules.

Live server and reloading

The live server code is another part where bun shines. Admittedly, the logic behind the server is very simple, but bun somehow makes it even easier.

In summary, the live server acts as a simple file server that injects a simple script into each html file it serves:

In the code, its minified into a single-line string that also replaces PORT with the actual port. This script establishes a websocket connection with the server and waits for a message to reload when a file is changed. The server was also extremely easy to setup:

To trigger a reload, we just need to publish a message in the live_reload channel. This is done in the body of callbacks passed to the fs.watch() function, which watches (duh) the files for changes. The use of channels also allows us to open multiple pages of the website simultaneously, and have them all reload on change.

The not-so-happy parts

HTMLRewriter input types

One little issue I had with HTMLRewriter was the fact that it worked on Response objects, so parsing code looked a little like this:

(yes, I'm reusing the same snippet from above)

which is not ideal, but not a pain either. It does accept a Blob or a Bun.BufferSource, but I'm not sure how to convert a Bun.file to either of those, or if there are even any performance gains from this.

Documentation

Bun is a shiny new tool that's already making great strides. But like anything new, it does not boast a good base of Stackoverflow questions like javascript, and you are left to figure things out yourselves. However, bun has guides, which I found very useful.

Closing

To put it shortly, writing the generator was a fun weekend project that's completely fine for my small site. However, if it motivates you to write your own, please preserve your sanity and do not impose stupid restrictions on yourself, like I did.

This is also my second post on this blog, and I appreciate any feedback over at me.aryaveer@gmail.com. I already have ideas for future posts, including a more in-depth post about HTMLRewriter and bun tooling in general. Cya :)

You can check out the source over at the Github repository.

← Back to /posts