I made this (thanks to Eleventy)
My enthusiasm for the Indie Web has rekindled my interest in developing my own website. While I think the way the Indie Web is promoted is still a bit too developer-oriented to have mass appeal, this hasn’t put me off getting stuck in. You don’t have to build your website from scratch to be part of the Indie Web, but I do relish the challenge.
This isn’t a tutorial, but I want to document the technologies i’m using, the choices i’ve made, and to highlight some of the features of my tech stack that currently fascinate me. If you’d like a more eloquent introduction to Eleventy, I would recommend this Eleventy video tutorial by Kevin Powell.
I’m using the Eleventy Static Site Generator
A Static Site Generator is a tool that can generate the HTML files that make up a website from content and templates. Eleventy is one such tool, but others exist such as Jekyll and Hugo. Static sites are very secure and very speedy: all that sits on the server are the HTML and CSS files. As the name implies, static sites are static. Typically any JavaScript in use is only used during build time rather than served to visitors. There’s always the option to use JavaScript to drive more advanced functionality but that’s beyond my needs currently.
I was previously introduced to Eleventy three years ago through Andy Bell’s excellent Learn Eleventy from Scratch series. However, I didn’t have a strong motivation to finish my website and quickly got distracted by other interesting but unimportant things.
This was also around the time I realised what a gulf there is between my comfort with the building blocks of the web - vanilla HTML, CSS and JavaScript - and the complexity of modern web development tooling. I can write the code and put it online, and my career requires me to have a good understanding of how the quality of the code impacts the user experience, but all the developer conveniences like CSS preprocessors (e.g. SASS), package managers (e.g. NPM), bundlers (e.g. Webpack) and API query languages (e.g. GraphQL) are beyond my current capabilities. Okay, I can just about work with NPM but there’s a whole level of stuff happening under the hood that I haven’t a clue about.
However, that’s why I like Eleventy: It feels close to writing raw HTML without actually having to write too much raw HTML. It’s also incredibly flexible in that I can add other things to my build process when I feel like it. I’m also really enamoured with how quick it is to get something working. Out of the box, Eleventy can take my Markdown files and generate webpages in hundredths of a second.
My eleventy site is built using a combination of:
- Content (Markdown)
- Templates (Nunjucks)
- Data (JSON)
- Collections, Filters, Plugins and Shortcodes (JavaScript)
- CSS
Markdown files
My blog posts and notes are created using Markdown.
Markdown, in case you’re not aware, is a mark-up language used to format text. I used it extensively in my last job. It’s very readable, using just a handful of symbols. For example, the following are a few common web content items, written in Markdown:
# Heading 1
## Heading 2
### Heading 3
**bold text**
*italic text*
[link text](https://www.straydogstrut.co.uk)
![alt text](image.jpg)
* list item 1
* list item 2
* list item 3
A blog post on my site looks like the following:
---
title: A proper introduction
date: 2024-11-02T16:00
tags: ["NanoPoblano", "NanoPoblano2024"]
thumbnail:
image: thumb.jpg
---
As you can probably tell, my first NanoPoblano post was rushed. I was writing a post about how I made this website and the words were nowhere near their end, so I quickly pulled together a half written post i’d started a few weeks ago. If we’re honest, the only thing I like about it is the title.
<!-- more -->
I’ve spent a lovely morning catching up with some of the first [NanoPoblano](https://cheerpeppers.wordpress.com/2024-team/) posts from my fellow Cheer Peppers.. and that’s when it struck me: I should probably introduce myself. I haven’t written here in so long that my welcome mat is covered in dust and debris.
That stuff between the ---
lines is Front Matter. It’s a way of adding metadata to my posts, such as the title, date and categories (tags). It’s also where I assign the featured image or thumbnail for that post.
This metadata can be used in my templates rather than hard coding information.
Nunjucks templates
Eleventy supports multiple templating languages, including Nunjucks and Liquid. I’ve stuck with Nunjucks as that’s what many examples use, but the syntax of the two is not that different.
A template is like a blueprint for my pages. It provides structure to my pages and a place for my content to appear. For example, my home.njk
looks like this:
{% extends "layouts/base.njk" %}
{% block content %}
<div class="wrapper">
<div class="content container">
<h1 class="section-title visually-hidden">{{ intro.headline }}</h1>
<section class="recent-articles">
<h2 class="section-title">Latest posts</h2>
<ul role="list" class="articles__list flow">
{% set latest_posts = collections.postsNotFeatured | limit(3) %}
{% for post in latest_posts %}
{% include 'partials/post-snippet.njk' %}
{% endfor %}
</ul>
</section>
{% include 'partials/featured-posts.njk' %}
</div>
<aside class="sidebar container flow">
{% include 'partials/profile.njk' %}
<hr>
{% include 'partials/category-list.njk' %}
<hr>
{% include 'partials/blogroll.njk' %}
<hr>
{% include 'partials/currently-reading.njk' %}
<hr>
{% include 'partials/badges.njk' %}
</aside>
</div>
{% endblock %}
That’s my homepage. Beautfully short and (hopefully) understandable.
The reason it’s so short is because of inheritence and includes:
Inheritance
The {% extends … %}
command allows me to chain templates together to create a more complex whole. My base.njk
template is where the surrounding code for the page sits. All the elements I want throughout my site, including the title and meta information, header and footer. All my major page templates extend from this base template:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE-Edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site.title }} | {{ site.subtitle }}</title>
<link rel="stylesheet" href="/css/styles.css">
<link rel="alternate" type="application/rss+xml" href="{{ site.url }}/feed.xml" />
</head>
<body>
{% include 'partials/header.njk' %}
<main>
{% block content %}{% endblock %}
</main>
{% include 'partials/footer.njk' %}
</body>
</html>
Those {% block content %} {% endblock %}
tag pairs, allow me to define how content from my Markdown files appears in my templates. They’re a more flexible alternative to the {{ content | safe }}
variable (which I also use to just pull in content).
Includes
I can make use of includes to pull in information from other templates. Nunjucks calls these modular templates partials, and I’m using several:
My base.njk
template pulls in the header and footer partials, while my homepage uses partials for featured posts and each of the items in my sidebar. I really love how modular this makes my code and how easy it is to swap things in and out.
This also means that my templates are reusable. The same post-snippet.njk
template that displays posts on my homepage can be used to display posts on my main blog page:
{% extends "layouts/base.njk" %}
{% set postListItems = pagination.items %}
{% block content %}
<div id="post-list" class="section container">
<h1 class="section-title">Recent articles</h1>
<ul role="list" class="articles__list flow">
{% for post in postListItems %}
{% include 'partials/post-snippet.njk' %}
{% endfor %}
</ul>
{% include 'partials/pagination.njk' %}
</div>
{% endblock %}
<li>
<article class="snippet">
{% if post.data.thumbnail.image %}
<img src="{{ post.url }}images/{{ post.data.thumbnail.image }}" alt="{{ post.data.thumbnail.imageAlt }}" class="snippet__image">
{% endif %}
<div class="snippet__content flow">
<h3 class="snippet__title"><a href="{{ post.url }}">{{ post.data.title }}</a></h3>
<div class="snippet__meta">
<p>{{ post.date | postDate }} | {{ post | readingtime }}</p>
{% set postCategories = post.data.tags %}
{% if postCategories %}
<ul class="list-horizontal">
{% for category in postCategories %}
<li>
<a href="/category/{{ category | slugify }}" class="category-pill">#{{ category }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% if post.data.page.excerpt %}
<p class="snippet__body">{{ post.data.page.excerpt | md | safe }}</p>
{% elif post.data.summary %}
<p class="snippet__body">{{ post.data.summary }}</p>
{% endif %}
<a href="{{ post.url }}" class="btn btn--primary">Continue reading</a>
</div>
</article>
</li>
I use a similar approach for my Notes and Now pages. The actually share the same timeline.njk
template and I use a conditional to decide whether to display titles and dates.
<section class="timeline">
<ol>
{% for item in collections[feedCollection] %}
<li class="flow">
{% if item.data.title %} <!-- now post -->
<div class="timeline-month"><h2>{{ item.data.title | safe }}<h2></div>
{% else %} <!-- note post -->
<div class="timeline-month category-pill">{{ item.date.toUTCString() }}</div>
{% endif %}
<div class="timeline-status">{{ item.content | safe }}</div>
</li>
{% endfor %}
</ol>
</section>
JSON Data
As well as pulling in metadata such as titles, URLs and dates, I can make use of data files to allow me to easily provide dynamic data. For example, I use JSON to define the items in my Blogroll. This means I don’t need to touch the templates to update these items:
{
"beloveds": [
{
"text": "Rarasaur",
"url": "https://rarasaur.substack.com/"
},
{
"text": "Behind the Willows",
"url": "https://behindthewillows.com/"
},
{
"text": "Cats and Chocolate",
"url": "https://catsandchocolate.com/"
},
{
"text": "James' Coffee Blog",
"url": "https://jamesg.blog/"
}
]
}
I’m doing a few other things with data files, such as driving my navigation, category navigation, badges, and reading list. In some cases, my JSON files could be better structured, but i’ve got something that works for now and that’s all that matters.
Collections, Filters, Plugins and Shortcodes
There’s so much I could say here but i’ll try to keep it brief for this post. Eleventy uses a config file to set up the various conveniences i’m using throughout my site. I configure here how Eleventy handles different files, setup my post excerpts, and my collections, plugins and shortcodes. My .eleventy.js
config file is really the heart that drives my website.
Collections
Collections are how Eleventy can group content. For example, in order to display my blog posts, notes, now items and category archive, I’ve created collections for each by cycling through their respective directories or by collating tags. Similarly, when I use tags in my Front matter to categorise my posts, Eleventy automatically creates collections from these. Collections are one of Eleventy’s most powerful features.
eleventyConfig.addCollection('posts', collection => {
// returns Markdown files in the src/posts/year/month/day/post-title/ directories in reverse order
return collection.getFilteredByGlob('./src/posts/*/*/*/*/*.md').reverse()
});
// Returns an array of tag names for my category archive
eleventyConfig.addCollection('categories', collection => {
const gatheredTags = [];
// Go through every piece of content and grab the tags
collection.getAll().forEach(item => {
if (item.data.tags) {
if (typeof item.data.tags === 'string') {
gatheredTags.push(item.data.tags);
} else {
item.data.tags.forEach(tag => gatheredTags.push(tag));
}
}
});
// return the tags, sorted alphabetically
return [...new Set(gatheredTags)].sort();
});
Filters
Filters are useful to change the output of a command. For example, I use the .reverse() filter on my posts and notes collections to get them in reverse order. I also use a dateTime filter to format my dates appropriately, and a limit filter to restict how many posts are shown on the homepage.
eleventyConfig.addFilter('postDate', (dateObj) => {
return DateTime.fromJSDate(dateObj).toFormat('dd MMM yyyy');
});
eleventyConfig.addFilter('limit', function(array, limit) {
return array.slice(0, limit);
});
Plugins
I’m using a handful of plugins at build time to create the HTML needed. This includes Luxon to format my dates, a reading time plugin, rssPlugin and Eleventy-img to optimise my images.
eleventyConfig.addPlugin(eleventyImageTransformPlugin, {
// which file extensions to process
extensions: "html",
// Add any other Image utility options here:
// optional, output image formats
formats: ["avif", "webp", "auto"],
// optional, output image widths
widths: ["auto"],
// optional, attributes assigned on <img> override these values.
defaultAttributes: {
loading: "lazy",
sizes: '100vw',
decoding: "async",
},
});
Truth be told i’ve had a really difficult time using the Eleventy-img plugin. I’ll write about that in another post, but for now i’ve got things working.
Shortcodes
Shortcodes are nice little conveniences you can add to your Markdown and templates to include HTML or other output. I’m using a handful such as one to display YouTube videos and my content warnings.
eleventyConfig.addShortcode("youtube", async function (id) {
return `<div class="video-wrapper">
<iframe width="640" height="385"
src="https://www.youtube.com/embed/${id}"
title="YouTube video player"
frameborder="0"
allow="accelerometer;
autoplay;
clipboard-write;
encrypted-media;
gyroscope;
picture-in-picture;
web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen>
</iframe>
</div>
`
;
});
eleventyConfig.addShortcode("content-warning", async function (topics) {
return `<div class="content-warning flow">
<h2>Content warning</h2>
<p>The following post talks about my experience of one or more topics you may find distressing,
including ${topics}. While I am comfortable talking about my experience, I do not assume the same for everyone.
If you need to give this one a miss, please do. There is no obligation to read any
of the content on my website. Please use your own discretion and keep yourself safe.<p>
</div>`
});
CSS styling
My initial styles are direct copy of those set up by Kevin Powell in that tutorial. While i’m familiar with CSS, I find it helpful to have a roadmap for things like ensuring responsiveness and setting up CSS Grid However, i’ve been modifying as needed and there’s definitely more to do from an accessibility point of view.
One thing I like about Kevin’s approach is he’s using Andy Bell’s Cube CSS methodology. I had started to read about this and I liked the promise of being able to fully utilise the cascade to create effective layouts. It’s inspired by BEM and allows you to use CSS to compose your layouts, making use of high level patterns and utilities, with little need to customise individual elements. It’s elegant and I like it.
I definitely have to get used to using this approach - some of my additions aren’t fully adopting the methodology - but I was very impressed with how quickly Kevin’s layout came together using Cube CSS.
I love using Eleventy
I appreciate this is quite a technical post. However, the key takeaway is how easy Eleventy makes it to throw some Markdown files in a folder and have them turn into webpages. Everything else - templates, data, collections, filters, plugins, shortcodes - is up to me what I use and how. It’s such a delightful technology to use and I’m still excited by making things appear on screen.
This is a post for this year’s NanoPoblano. From the Cheer Peppers website:
“National Blog Posting Month (NaBloPoMo) is a month-long blog event in November, celebrated by writing a post every single day. This tradition sprouted many other traditions– small groups that regularly go into the fray together, or even folks who level up and do it with the challenge of a theme. A million and a half years ago (est.), we started our own nano-tradition, and call it NanoPoblano. It’s different than most others.
It’s less organized. There’s less pressure. It’s more about the community than the challenge. We aren’t pulling people off rosters or anything if they can’t keep up. There aren’t qualifiers to sign up. You can be new. You can have a photo blog, or a recipe blog, or a haiku blog. Everyone can be a Participant.
We’ve taken “Post every single day” to mean– support blogs, every single day. That usually means writing every day (and that’s a challenge that everyone benefits from trying!) But we also recognize that the ones who read and cheer us on are important too. We have CheerPeppers who don’t necessarily post that month at all, but try to read or like or comment on a post of our participants every day.
We have Participants who do re-posts. We have Participants who spend the 30 days committed to bringing life to their blog, even that if that means not writing some days, while they work on their themes or leave comments from folks they’d like to build community with.”