25 September 2024 11min read

Back to the basics with Genesis

Recently, I finished an almost final draft of my novel and decided it was time to refresh my online presence and website. Instead of starting my next novel, I dove into creating the next version of my site, what I call alejandromp4. And, of course, you know what that means… using a new static site generator! Yes, I have a problem with focusing my hobbies; I’m working on it.

Let me first tell you what was the status quo of the version 3 of my site. It was built using Publish, a static site generator written in Swift. That worked really well as Swift is my favorite language, but more than that, it’s the ecosystem where I spend most of my time. This helps me avoid surprises when I come back to the website after a while, something I disliked about previous incarnations based on Ruby or Node, ecosystems that seem to break every time I had to publish an article.

But Publish wasn’t all roses. I have some peculiar requirements for my site around taxonomies and multiple sections that needed to work well together. Although Publish offers some customization options, it wasn’t enough. I quickly had to change some things in Publish, which meant discarding the nice defaults that come with it. I also had to fork it and open some internal functions to avoid repeating what was already built. Over time, I grew comfortable with that codebase and my customizations, but it’s not something I want to deal with long-term.

So, I thought it was a good occasion to look for a replacement.

The current sphere

Recently, another contender has appeared: Ignite. There are many others, but this one has the backing of a prominent figure in the ecosystem, which has propelled it to popularity. I spent some time with it, and it’s not that dissimilar from Publish—in the end, all static site generators are more or less the same. There are things I really like about Ignite, the documentation and the easy of use is really something to admire. But ultimately I decided it wasn’t for me. In an effort to be easy to use out of the box, it’s too tight for my taste. For example, to build pages, you need to accept using Bootstrap and a DSL, which is nice for a quick start but I needed the flexibility to not only use another CSS system (because I don’t like Bootstrap) but also use existing HTML templates that were already good to go.

I looked around for a bit and found other interesting alternatives with intriguing declarative systems to build the site in various shapes and forms. This is a trend in modern static site generators—a sort of declarative way of specifying pages and content for the generator. It’s an approach that aims to simplify the configuration process, though it comes with its own set of tradeoffs.

Ultimately, I came to the conclusion that it was time for me to tackle the problem myself. But I you should probably look at the mentioned projects if you are in need of a static site generator, they are very good and probably more inline what you expect.

Why Genesis

With a few years of experience and having seen how other generators work, I thought it wouldn’t be too hard to make one myself. In fact, the core idea is an old one, as is the name, but I never pursued it because I didn’t want to have something to maintain.

Now, I was not happy with my current situation, a forked engine with the original project barely maintained. And if I had to switch to another project that, even if promising today, could be abandoned any time, I wouldn’t be in a better position. So the tradeoff of making my own is very different.

But if I have to build my own site generator, and make sure doesn’t take too much time from writing the actual content for the blog, I need to make a few decisions that will help me now and in the future. So I defined a core rule: simplicity.

This meant no fancy declarative rule system, no fancy HTML DSL, no building things from scratch, etc. But the best thing came with the realization that doing things that way woudl actually bring me all the benefits that I found lacking in other solutions. It was a win win sitaution!

So, armed with valor, some open-source code that I could reuse, and a few ideas in my mind, I sat down and started writing requirements to not lose the north star, drawing some diagrams, and finally coding.

The Goals of Genesis

Simplicity

The first overarching goal was to keep things simple—no, really, as simple as possible. I knew that remaking my entire site wouldn’t be trivial; there’s a lot of custom logic and hacks I’ve done to Publish that, although simpler in my own engine, still had to be ported. I also have some peripheral tools for my site (a CLI to work with it and a tool to create posters), but I was confident those could be moved directly.

I acomplish this by making the API very clear and specific, without fancy declarative engines or DSLs. What you get are just a few small protocols and a bunch of functions that you can call.

Flexibility

The other goal was to keep things flexible. Not to make this a fancy library, neither for my future self. It wasn’t flexibility for the sake of flexibility or purity. It was flexibility because I already know that my current requriements have some complexity that requires it.

Learned from experience, I don’t want to have defaults that become irrelevant as soon as you want to customize things. And if you want to customize things you should be able to reuse as much as the engine as possible without having to fork and make things public. This means that I don’t want to have a single call that does everything, but instead the tools that I can compose as needed.

Genesis provides APIs to load content and generate pages using simple function calls. It’s procedural on purpose. Just call the functions however you want without being tied to a specific structure or rule system.

For example there is a clearBuildFolder() function that does what the name indicates. But you don’t have to call it if you don’t want, nobody is forcing you. Of course when publishing your site you probably want to do a clean generation, but while iterating and previewing it? It’s just a waste of time. Or not. You decide. That’s the point.

Pages are just Strings

What I wanted for sure the new generator to give me was the flexibility to handle pure HTML templates directly so I could take a beautiful template made from a good designer and just use it. But also I knew that eventually I would want to write some HTML in Swift with one of the fancy DSLs that everybody uses. But which one was another story. Beacuse of that I didn’t want to imbue the engine with any DSL, I wanted to engine to just work witht the minimum thing that needed.

So what’s the only thing that is needed as an input to generate a file that can be served? A String.

So instead of using a fancy DSL for typed Swift HTML, Genesis simply works with strings. This simplicity opens up a world of possibilities: from using existing HTML templates to experimenting with various Swift HTML DSLs, all without changing the core engine.

The advantage is that acomplishing this is also very simple, you just need a function that returns a String.

func render(context: Context) async throws -> String {
    """
    <html>
    <body>
    <p>A page in your site is just a String. Easy.</p>
    </body>
    </html>
    """
}

I know many will be screaming on seeing this, but honestly I’ve written most of this site you are reading in Swift String interpolation and is totally fine. Reusing “components” is just making a separate function that returns a String. It doesn’t get simpler than that.

And the best thing is that I can easily add fancy DSLs on top. It took me five minutes to add suport for a new DSL library, elementary, and a few more to try another one. Adding support for loading tempalted files is also trivial.

No web dependencies

Something else I didn’t want was to tie the engine to specific web frameworks. For example, I’m not a fan of Bootstrap so that wouldn’t come prebacked. The old version of the site used Bulma but the previous version of my author site used TailwindCSS. So the engine doesn’t care what you use, again, it simply expects a String. It’s actually very nice how different requirements coalesce into the same goal.

Content Loaders

Genesis doesn’t automatically load any dynamic content for you. I don’t want it to check specified folders for predefined files. Instead Genesis expects you to define ContentLoaders to load dynamic Content. This allows you to determine what the content is and how it’s loaded, from markdown files, a database, or a remote CMS. The engine doesn’t care about it.

A simple async function is all you need to implement. Even what “content” is is not defined, you bring your own.

struct BlogLoader: ContentLoader {
	func load(context: Context) async throws -> sending [any Content] {
    let contentDirectory = await context.contentDirectory
    let contents = try FileManager.default.contentsOfDirectory...
    return try contents
        .map { BlogPost(...) }
}

Pages

Besides loading content, the other core concept of the engine is generating pages. A Page is a type that implements the render function explained above. That’s it. This makes it very trivial to generate single pages for your site, things like the Home or the Author page.

But there are pages that need to be rendered multiple times, for each instance ot the loaded content. like each post page. For this the engine has another simple protocol, the PageProvider. And again, it’s a very simple requirement: just return a list of pages.

func source(context: Genesis.Context) async throws -> [any Page]

Simple Core

These few basic requirements are what defines the core of the engine, which also makes it very easy to understand what’s the process of generation and how you interact with it.

  1. A Context that is passed around and offers a set of functions to manipulate the site.
  2. All the Content, which can be anything, is loaded into the context via Content Loaders.
  3. It renders and writes to disk Pages that can be single instances statically given, or
  4. Page Providers can instantiate multiple pages from the content loaded in the context.

And the reality is that that’s all a static site generator is at its core. When you put it this way it’s not that scary anymore. If you even think about it, and I have, half of these protocols are not even necessary from how simple they are. But I found that having them helped me have a better mental model of the core of the engine.

Expandable Shell

The beauty of this simple architecture is that it let’s me expand it as needed. I already mentioned above that adding a DSL for HTML is very simple, but there is more that can be done.

Standard Pages

To reiterate, the core of the engine doesn’t do anything for you. You might expect RSS, sitemaps, etc to be generated, like it happens in most other engines. But in Genesis that’s up to you, you give it the Pages that you want to generate.

But of course, RSS and Sitemaps are pages that are quite standarized. So for that I took some code from other engines and with a few tweaks it was working fine for my site. Now this are bundled in the library but is up to you to use them or not.

Markdown

This is probably the first thing anybody wants from a static site generator, so you might be surprised that, again, Genesis doesn’t have support for Markdown fiels embeded into its core. That’s another consious decision. I don’t want to make the mistake of embeding a specific library that will make things harder for the sites build with the engine, I’ve already suffered that in the past.

But agian, Genesis doesn’t need this embeded, it can be added from the outside because its core is very small. In my case I’ve decided to use Apple’s swift-markdown because I think is a safe bet nowadays, it makes it trivial to parse Markdown fiels intro an AST that then can be transofrmed into a String.

Crucially, the fact that I have full control over the Markdown library used and of the type that defines a “post” means that it has been trivial to do something that I’ve found impossible in any other site: keep the markdown tree trough all stages of generation so it can be tweaked in specific pages. This small thing has given me so many headaches over the years that I can’t believe how trivial it is in Genesis. Just because you have the contorl and you decide.

Conclusion

By going back to the basics, I’ve been able to build a static site generator and port my site to it in a few days. I love how Genesis offers a flexible and powerful static site generator that doesn’t constrain you to specific workflows or technologies. It’s designed to grow with your needs, allowing for simple setups for basic sites and complex configurations for more demanding projects.

I haven’t optimized for a marketable experience, so while it may not have the polish or out-of-the-box features of some popular generators, Genesis provides the building blocks for creating exactly the site you want, without fighting against pre-defined structures or limitations. It’s a testament to the power of simplicity and the value of understanding your tools from the ground up.

Of course this is just the begining, I will continue to develop and use Genesis for my own site, I’m excited to see how it evolves. I already have a few ideas to add more batteries while keeping the core simple.

And finally, this is not a sales pitch. As I said at the start, you probably want to check the existing generators with bigger communnities, but if you like things simple and are not afraid of getting your hands dirty you can try Genesis. Or maybe if you want to build your own this post can serve as a guide and word of advice: keep it simple!

If you enjoyed this post

Continue reading