Jack Jazrawy-Brown / The 11ty Rewrite

AboutBlogContact

The 11ty Rewrite

In a sudden and unexpected burst of productivity, I've just completed a ground-up rewrite of this site. The first version was built with Jekyll, purely because GitHub pages supports it out of the box and I was able to create a functioning site with it pretty quickly. Since then I've become much more confident with web technology, and it felt like a good time to start from scratch and build something lean and elegant that I could use into the future.

My basic philosophy for the rebuild was to include the bare minimum for my requirements and nothing more. There's nothing wrong with Jekyll, but it never quite clicked - the project-structure and configuration always felt rather messy to me, and the GitHub pages integration is somewhat locked down. I wanted something a little more portable and much leaner, and after some research I chose 11ty as the basis for blog v2. 11ty is closer to a tool than a full framework, essentially offering a pipeline that ingests content in a variety of templating languages and spits out webserver ready HTML. It is nonetheless incredibly powerful, with a clever JS based configuration system and plugin library ready to handle effectively any static site use case. The complexity of an 11ty project grows to match the complexity of its requirements and no further - perfect for my lofty minimalist goals.

This post is intended to be a quick tour of some of the cool things I was able to achieve with 11ty. I'm very pleased with how clean and concise the codebase for this site is after the rewrite, and I have much greater confidence that future tweaks and improvements will not cause anything to explode.

Layouts

Layouts are a powerful feature of 11ty that allow content to be wrapped in a reusable template, eliminating the need for copy-pasting boilerplate across numerous pages. At the time of writing, this blog has three layouts:

  • A universal layout containing the title, navigation bar and theme switching JS.
  • A layout for normal routes, with page content aligned to the left.
  • A layout for posts, with page content centred. This layout also contains a dynamic title section and some schema.org stuff to allow reader view to identify each post's title and author.

Both secondary layouts use chaining to extend the base layout.

Layouts are utilised by setting the layout property in a page's front matter, which will cause its content to be rendered in that layout. And to prevent the need for copying layouts everywhere they're used, 11ty provides directory specific data files - so for example, in my /posts directory, I have a posts.json file with the following contents, which causes every file in that directory to have its layout property automatically set:

{
  "layout": "post.liquid"
}

Layouts can be used to set default data properties too. My post layout configures a default permalink based on post title, a default author (me) and adds the page to the blog collection:

---
layout: base.liquid
tags: blog
permalink: "blog/{{ title | slugify }}/"
author: Jack Jazrawy-Brown
---

Combined with the layout field in my data file, this means all I need to add to a post's front matter is a title and a date - everything else is inherited. Minimalist!

---
title: "Home-Baked Bread"
date: 2024-08-02
---

Collections

11ty collections are groups of pages with a common tag, which can be accessed in template markup on the collections object. This site uses collections primarily for navigation, with a route collection for top level pages (e.g. "About") and a blog collection for blog posts. These collections can then be iterated over in templates - for example, the following renders a link to any page with tag: route in its front mater:

{%- for route in collections.route -%}
  <a href="{{route.url}}">{{ route.data.title }}</a>
{%- endfor -%}

And this renders a link and date for anything in the blog collection, most recent first:

{%- for post in collections.blog reversed -%}
<div>
  <a href="{{post.url}}">{{ post.data.title }}</a>
  <span>
    {{ post.data.date | date: "%b %d, %Y" }}
  </span>
</div>
{%- endfor -%}

Using this approach means any link menus can be set up dynamically at build time, which avoids having anything hardcoded and also allows future additions to be made with ease.

Styling with Tailwind

The giant main.scss file from blog v1 was one of the main things I wanted to ditch, as it was not remotely maintainable and had some ugly visual issues. I chose to use Tailwind for v2, as it's become my go-to tool for managing CSS in web projects. The immediacy (and lack of a giant centralised point of CSS failure) really clicks for me, and the more comfortable I get with the syntax the more Tailwind feels like the way CSS is supposed to be.

11ty doesn't offer a Tailwind plugin or build step out of the box, but the standalone CLI can still build a CSS file from the classes in the blog's Liquid templates. However, this approach doesn't integrate with 11ty's hot reloading feature by default, something which would have seriously hurt the ergonomics (and fun) of development. I conducted a brief search for prior art - and some doomed conversations with Gemini - but the guides I could find mostly required a separate shell with Tailwind running in watch mode, and needing two separate commands to start a dev server felt like unnecessary overhead.

Thankfully, 11ty offers compilation events which allow you to run code at specific points in the build process. Using the eleventy.before event, I was able to run the Tailwind CLI via execSync whenever 11ty builds the site, meaning pnpm start is all that's needed to get up and running:

eleventyConfig.on('eleventy.before', async ({ dir }) => {
  try {
    execSync(`npx @tailwindcss/cli -i ./main.css -o ./${dir.output}/main.css`, { stdio: 'inherit' });
  } catch (error) {
    console.error(`Error building CSS:`, error);
  }
});

This is perhaps slightly hacky - but it's set and forget, and works perfectly whenever I'm working on the site.

Plugins

11ty has a suite of excellent first-party plugins, and this blog uses two:

  • @11ty/eleventy-plugin-rss for generating the RSS feed
  • @11ty/eleventy-plugin-syntaxhighlight for styling code blocks.

RSS basically works out of the box, and tweaking syntax highlighting (which uses Prism) is done by copying a theme from here and including it in the blog's build. I chose to use the Atom Dark theme, making a few minor customisations; the most significant of these was to swap out the theme's colour palette for that of Tailwind. This palette is exposed via CSS variables (for example var(--color-slate-100)) making it simple to align a non-Tailwind stylesheet with the rest of the blog's design.

Inlining

Inlining resources like CSS and JavaScript reduces the number of network requests needed to render a page, which can improve load times. While in my case it's rather unlikely to make a difference, I was quite pleased with this trick for including the pulling in resources at build time. As it turns out, Liquid's include tag will accept whatever file you want, not just other templates. In practice, this means that I've been able to store JS, SVGs and CSS files as separate files, retaining benefits like editor syntax highlighting while avoiding the need for additional load requests. For example:

<style>
  {% include "prism.css" %}
</style>

<script link rel="script">
  {%include "theme.js" %}
</script>

For larger projects, 11ty offers guides for minifying and inlining code, but for my site's needs this approach has proved to be more than adequate.

Theming

The only new user-facing feature of blog v2 is the theme toggle in the navbar, which allows the site's colours to be persistently toggled between a light and dark mode. Adhering to a philosophy of progressive enhancement, I wanted to keep the use of JS to a minimum and preferably away from static content where it's not needed - however, I conceded to twenty-ish lines in the base template to get this working.

Tailwind offers an exceedingly simple dark mode setup based on a root element class, and once it's configured all that's needed is a call to classList to toggle the theme (note that this must be done at the top of the DOM to prevent flashing):

theme = localStorage.theme
document.documentElement.classList.toggle("dark", theme === 'dark');

I also added a quick sun/moon theme toggle to the navbar using SVGs from Material Icons. This uses Tailwind's dark: selector to control the visibility of each icon, which also avoids any JS based element toggling.

<button onclick="toggleTheme()">
  <span class="dark:hidden">
    {% include "moon.svg" %}
  </span>
  <span class="hidden dark:block">
 {% include "sun.svg" %}
  </span>
</button>

Deploying

The final part of the blog v2 journey was to lease a proper domain (goodbye jackjazb.github.io!) and migrate off of GitHub pages. I chose CloudFlare pages for a number of reasons:

  • It can deploy directly from a GitHub repo with no manual configuration
  • It has a ready-made 11ty integration
  • It integrates very nicely with CloudFlare domains, avoiding any DNS configuration
  • The free tier is very generous - I'm unlikely to exceed it unless this blog gets very popular

After leasing jackjazb.space, getting my site up and running was literally a few clicks - doubtless the easiest deployment experience I've had!

I don't really have anything negative to say about 11ty - the docs are excellent, the tool itself works brilliantly and the whole experience of building blog v2 was a lot of fun. Mostly thanks to 11ty, it's one of the rare occasions where I feel I managed to avoid cutting corners and build something that feels elegant and minimalist - from a code perspective at least!