Most of the websites I build are simple, static, multi-page informational websites. I don't want to have to host a full backend, and I generally want to avoid JavaScript, so I have to choose between writing a lot of HTML by hand or using a static site generator (SSG). Using a static site generator seems like the obvious choice, but I've generally been unhappy with them (until now ;).

The Story

Skip this if you just want to read about the generator I wrote

There are tons of static site generators around; Jekyll and Hugo are some of the biggest, and the only two that I've used. Both of them make it easy to find a template, fill it with content, and not have to worry about the rest. This is the happy path, and it works really nicely for most people, including me in many cases. However, there were always cases where readily available themes didn't work for me. For example, my tutoring business's website needed a few custom, non-blog pages with a consistent header and footer, but it still had to look unique. SEO was also really important for it. I could've created a custom Jekyll/Hugo theme, but that comes with a learning curve, and honestly I've been scared away by the documentation every time. Why would I spend the time learning all of that when I could just write the HTML by hand and use some basic scripts to get consistent headers and footers?

To start with, I did exactly that. I wrote a header.html, footer.html, and a Python script to pre/append them to every page and make some convenient text variable substitutions along the way. It was only around 50 lines long, and I called it cat.py.

It worked pretty well for a while, but, as you might expect, it didn't quite scale. As the site got more and more pages with different quirks, the file kept growing in length and complexity. I'm still using it, but it kind of sucks. At some point, I realized that a good portion of the features I was implementing were already part of Flask.

The next site I had to write with similar constraints was simple-physics.org. I got so carried away writing the generator for it that I got bored before finishing the actual site. The generator turned out really well though, and I ended up building on it for this blog site.

After realizing that what I really wanted was to use a web microframework like Flask to write these sorts of websites, I looked into what it would take to implement some of the more complicated features like a nice routing interface and a function for each route. I ended up implementing a lot of it in a web library for my own language. The language didn't have any HTTP capabilities to start out with, so it was just a static site generator to start with. Then, once adding HTTP support to the language, I turned it into a server library by copy-pasting the gen_site function at the core of the SSG.

Then, I realized I could just do the same thing in reverse to turn a good microframework into a good SSG. It seems pretty obvious in retrospect, but I hadn't seen it done before. Since then, I've learned that it has though; Frozen Flask does the same thing as far as I can tell.

The Solution

My goto language for anything web related nowadays is Elixir. It's nice to work with for a lot of reasons, including Plug, a library to "compose web applications with functions". That seems kind of vague, but I use it essentially as a nice router library that makes it really easy to write middleware.

At first, I tried writing a plug to simply download the result rather than sending it to a web library like Cowboy or Phoenix as usual, but then I realized that using a minimal framework like Cowboy basically gets me hot reloading for free. Since I was already using Cowboy, I decided not to overthink it, and wrote a small function to just download every page from a list to its corresponding location. It's stupid and simple:

def gen_pages() do
  System.cmd("tailwind", ["-i", "./CSS/base.css", "-o", "./public/static/CSS/base.css"])
  ...
  for page <- pages do
  resp = HTTPoison.get!("localhost:4000/#{page}")

  page = if page == "", do: "index", else: page
  path = "public/#{page}.html"
  File.mkdir_p!(path |> Path.dirname())
  :ok = File.write(path, resp.body)

  IO.puts("Generated #{page}.html")
  end
end

All this snippet does is download needed pages from a local cowboy server, so everything else I wrote is applicable to any generic cowboy site. You could also just use this snippet for any other web framework. Here are some reasons I went with Elixir though:

  • Elixir comes with a templating system called EEx that's extremely flexible and allows arbitrary elixir code execution within a template
  • Plug is a really powerful framework; I wrote a plug middleware that fixes some issues with HTML paths such that routes don't have to end with .html for the generator to work
  • I can open an iEx shell while writing server code for easy hotloading

There are some pretty significant features I'd like that are still missing though:

  • UI Components - Right now, I just use basic Elixir functions that return an HTML string for reusable components. It works fine for the most part, but I'm unable to make components that wrap other HTML in a reasonable way. I think I could write a macro for it, but I might also just use Vue.js or Web components. In the worst case, I would have to parse it out myself and manually substitute.

  • Server rendered syntax highlighting - I'm using highlight.js right now. It works great, but it's the only thing that I use JavaScript for on this site other than the gravity dots, which aren't really important.