Ha.nnes.dev
A static site generator is just a pure function
What I learnt by rewriting my website five times
2023-09-26
Most websites I use are web apps instead of web pages, which either require a lot of client-side JavaScript, or a server for dynamic content. If you donât need interactivity, a static site generator (SSG) can simplify a website with many pages.
My website
For my website, I started in 2018 with Pelican before moving to Hugo which are both template-based SSGs.
These are some of the simplest and most common kinds of SSGs, you just
sprinkle your HTML with {{ special templating syntax }}
like jinja2 and the SSG
generates your website. You can insert things like variables, HTML
snippets or rendered markdown and run some control flow logic on the
data like conditionals and loops. For most websites, templates strike a
good balance of high power for their low complexity. For example, my
website was initally just a homepage and a few sub-pages written in
markdown, which is exactly the happy path for these kinds of tools. Once I decided
to do some more complex processing to the input data, the template
expressions became long and inflexible.
The alternative to a âtemplate-basedâ SSG is a âframework-basedâ SSG. These SSGs use dynamic frontend web frameworks that are designed for web apps, but instead of dynamically rendering the page at runtime, they statically render the entire site at build time. I used Gatsby which is based on React, and uses JavaScript/TypeScript for all the data transformations. Because you get to use a âproperâ language to write your site, you get a lot more freedom to do whatever you want at build time, and Gatsby integrates a data store that can be queried in your components using GraphQL. I managed to build the site that I couldnât with Hugoâs templates, but the complex pipeline of processing data was slow, and I occasionally wanted a slightly more powerful query language than what Gatsbyâs GraphQL system offered. The biggest hurdle was when I wanted to step outside what the existing plugins offered, I found extending the engine very difficult. In fact, I found it so difficult that it made me wonder if all this complexity was worth it and if it might be easier to just write my own static site generatorâŚ
A component is just a pure function
Following in a long tradition, I wrote my own SSG. I liked how it felt to mix normal code with HTML elements, but didnât know how Iâd go about implementing that in another language, and I wasnât sure how to combine HTML elements into reusable components. The solution to this problem is obvious to anyone whoâs used Elm or something similar before â an element tag in React is just a pure function that returns an HTML string, and components are just compositions of these functions!
# A simplified example of what a tag function might look like
function tag(name, inner_html; attributes)
= join(["$k = \"$v\"" for (k,v) attributes], " "))
attributes_joined return "<$(name) $(attributes_joined)>$(inner_html)</$(name)>"
end
I decided to build my new SSG in Julia using Genie.jl to output the HTML
string. Juliaâs multiple dispatch and do
notation combined to make an (in my opinion) pretty readable DSL,
similar to React.
div(class="box") do
[h1("A heading"; id="title"),
p("A paragraph with some ", b("formatting"), "."),
]end
I got most of the way through writing my new SSG, but development slowed a lot near the end. I had made it very difficult to manage all the side effects like downloading the font files, downloading the blog posts to a temporary directory, unzipping them, and then making sure all the right files were written to. I also struggled to pass the right information to each component, which made me miss Gatsbyâs slow and limited GraphQL queries.
A static site generator is also a pure function
In the last few months, Iâve become very interested in the Roc programming language. One of the things that attracted me to it is that it is a completely pure language, meaning it is impossible to do any side effects in Roc. Roc solves this by making each program have an associated âplatformâ, some non-Roc code that essentially calls your Roc code like a library. Iâve found this concept of separating the effectful IO from the core logic very useful for simplifying complex programs. It gives me the ability to collect all the input effects like file reads and network requests into one place, put the output effects in another place, and then focus on all the parsing and data transformation logic in between. Working in this âfunctional core imperative shellâ style led me to realise that an SSG is also just a pure function, in this case a function from blogposts, CSS and fonts to a list of files and their contents.
Following this logic, the new SSG I wrote has three steps: input, pure transformations, and then output. In the input step, I download all the assets, read the content files and save it all into a big JSON file as a cache. In the second step, I read this big JSON file into a typed object and use that data to build all the pages and assets. Having all the inputs in one variable gives me an alternative to Gatsbyâs queries: by passing this object to every page and component, I can âqueryâ it with normal Python syntax. For example, to get a list of the blog post titles, I can use a Python list comprehension:
= [blogpost.title for blogpost in data.blogposts] blogpost_titles
The result of this pure build step is a dictionary object that maps a file path to the contents of that file, either an HTML page or an asset like an image or font file. The final output step just takes this dictionary and writes all the pages and assets to their designated file paths.
Rewriting again
I used Python for my SSG, which doesnât automatically manage side effects, so I had to just be careful with where I did side-effects. It wasnât a problem because my SSG is small enough to fit in my head, but I can imagine how a pure language like Roc would give this same level of confidence to a large team where no one has a complete understanding of the codebase.
If web components and SSGs are like pure functions, then what else is? I hope by writing more Roc code Iâll find out :)