How This Blog is Made

Long story short, this blog is im­ple­mented us­ing Eleventy, specif­i­cally by us­ing eleventy-starter as a start­ing point. Feel free to check out the code.

Before land­ing and set­tling on Eleventy I checked out the ex­tremely pop­u­lar Gatsby. As some­one ac­cus­tomed to React and Typescript it seemed like the nat­ural choice. Going through the docs was easy enough, and I even found a theme that I ended up mostly keep­ing for the fi­nal im­ple­men­ta­tion.

However, I found the API com­plex and con­fus­ing. While I was fa­mil­iar with most of the frame­works used, I could­n’t wrap my head around how it all fit to­gether. In par­tic­u­lar, im­ple­ment­ing a sim­ple col­lec­tion of posts took way too much cer­e­mony and in­volved a GraphQL API. While I love GraphQL, it all seemed overkill to me, and so I grav­i­tated to­wards Eleventy be­cause it seemed like the sim­plest tool for the job.

A Look into Eleventy

Eleventy was a plea­sure to work with. I know there are other great so­lu­tions out there (Hugo and NextJS come to mind), but I re­ally did­n’t have many com­plaints while build­ing this blog. To learn more about the ba­sics, I rec­om­mend the tu­to­ri­als or A Brief Tour of the Eleventy Static Site Generator by Digital Ocean.

11ty is easy to use, does­n’t get in your way and spits out ex­actly what you put in, so there’s no sur­prise or hid­den code bloat. At its most ba­sic, 11ty just com­piles files it finds from your work­ing di­rec­tory into sta­tic HTML files. Plus, since it’s writ­ten in JavaScript, you gain ac­cess to the whole of npm in terms of pack­ages you can use in your pro­ject.

As a sam­ple, this is ba­si­cally how you would im­ple­ment a page show­ing your col­lec­tion of posts in Eleventy.

First you would cre­ate a file.

layout: post.njk
title: My First Blog Post
date: 2019-11-30
tags: post

My first blog post content.

And be­cause of the tags field, that post con­tent and meta-data is avail­able in tem­plates to ren­der in

{%- for post in | reverse | limit(postsListLimit) -%}
<li class="py-3">
<a href="{{ post.url | url }}">{{ }}</a>
{{ | dateReadable }}
<p class="line-clamp py-1">
{% excerpt post %}
{%- endfor -%}

What I like about Eleventy is that it comes with sane de­faults, and that the guts” of the blog are taken care of for you.

The Stack

Eleventy pro­vides the skele­ton for cre­at­ing a blog, but does­n’t have opin­ions on styling, bundling, etc…

For styling I used Tailwind CSS with cus­tom fonts, and a vari­ant for dark-mode. This kept the cus­tom styles I had to write in the pro­ject to a min­i­mum.

For the bundler, I opted for ParcelJS. While Eleventy takes care of pro­cess­ing the mark­down and HTML, Parcel takes care of pro­cess­ing the Javascript, CSS and even mini­fies and op­ti­mizes my im­ages.

Finally, for the lit­tle cus­tom Javascript on the site, I used Stimulus (created by the folks at Basecamp). I can’t say that it was a con­cious choice — it was what came by de­fault in the starter, and worked well enough.

Turbolinks is in the mix as well.

All this is de­ployed to Netlify! The few icons you see are all from Feather Icons.

Even when go­ing deeper with Eleventy, and try­ing to im­ple­ment more cus­tom func­tion­al­ity, I’ve found I can get things done rel­a­tively quickly. Below are some ex­am­ples.

Creating Components”

If you’re used to writ­ing React, you’re fa­mil­iar with cre­at­ing components”. This is not 100% sup­ported in Eleventy, but you can get very close (and there is a GitHub is­sue about it). While I’ve seen a few ap­proaches, I ended up us­ing the one I found most promi­nently used in the of­fi­cial eleventy-base-blog. The gist is to cre­ate par­tial tem­plates, and in­clude them in full pages or lay­outs. We saw an ex­am­ple of a par­tial tem­plate ear­lier. This time around, you may no­tice that the tem­plate de­pends on an ex­ter­nal vari­able postsListLimit, which will limit the num­ber of posts ren­dered when avail­able.

{%- for post in | reverse | limit(postsListLimit) -%}
<li class="py-3">
<a href="{{ post.url | url }}">{{ }}</a>
{{ | dateReadable }}
<p class="line-clamp py-1">
{% excerpt post %}
{%- endfor -%}

The limit fil­ter is avail­able be­low.

eleventyConfig.addFilter('limit', function (array, limit) {
if (!limit) {
return array
return array.slice(0, limit)

Using your par­tial tem­plate, and defin­ing postsListLimit, can be seen be­low.

<div class="divide-y divide-gray-500 mb-5 mt-8">
<div class="flex flex-row justify-between py-2">
<h2>Latest Posts</h2>
<a class="link self-center" href="/posts">Read all posts</a>
{% set postsListLimit = 3 %} {% include "postslist.njk" %}

As long as your par­tials are in your _includes di­rec­tory, you’ll be able to im­port them. This feels close enough to im­port­ing a React com­po­nent and ren­der­ing it with props.

Dark and Light Mode

A fea­ture you see al­most every­where nowa­days is the abil­ity to switch be­tween dark and light mode. While Tailwind CSS pro­vides an ex­am­ple of im­ple­ment­ing dark mode with a me­dia query for browsers and op­er­at­ing sys­tems that sup­port it, I wanted to cre­ate a sim­ple tog­gle (the moon or sun icon in the up­per right cor­ner of this blog).

On the styling side, there is a plu­gin called tail­wind­css-dark-mode that does a lot of the heavy lift­ing for you. These are the rel­e­vant parts of my tailwind.config.js.

const { theme, variants } = require('tailwindcss/defaultConfig')

module.exports = {
purge: ['./src/assets/js/**/*.js', './src/**/*.njk', './src/**/*.md'],
theme: {
extend: {
fontFamily: {
primary: ["'Inter'", ...theme.fontFamily.sans],
secondary: ["'Noto Sans'", ...theme.fontFamily.sans],
variants: {
borderColor: ['dark', 'dark-hover', ...variants.borderColor],
textColor: ['dark', 'dark-hover', ...variants.textColor],
backgroundColor: ['dark', 'dark-hover', ...variants.backgroundColor],
plugins: [require('tailwindcss-dark-mode')()],

The variants sec­tion tells Tailwind CSS to cre­ate dark and dark-hover util­ity classes for bor­der, text and back­ground col­ors, which is all I need. As you might ex­pect, the plugins sec­tion im­ports and uses the plu­gin. What this means is that you can ap­ply CSS class names such as dark:bg-gray-900 or in my case, write CSS like this.

.mode-dark {
body {
@apply text-white;
.link {
@apply text-gray-600;
.link:hover {
@apply text-white;
p {
@apply text-gray-400;
p a {
@apply text-indigo-300;
label {
@apply text-gray-600;
h3 {
@apply text-gray-100;
blockquote {
@apply border-indigo-300;

That .mode-dark CSS class is the de­fault class used by tail­wind­css-dark-mode, but how does that class get added to your <html> el­e­ment? That is en­tirely up to you. In my case, I went ahead and im­ple­mented it in Stimulus. The con­troller is a sim­ple class that al­lows you to tog­gle dark mode on and off. It also per­sists the state to localStorage, so that you can main­tain the same mode across ses­sions.

import { Controller } from 'stimulus'

export default class extends Controller {
static targets = ['lightToggle', 'darkToggle']
initialize() {
const isLightMode =
(localStorage && localStorage).getItem('isLightMode') === 'true'
if (isLightMode) {
useDarkMode() {
localStorage.setItem('isLightMode', false)
useLightMode() {
localStorage.setItem('isLightMode', true)

The cor­re­spond­ing HTML is:

<div data-controller="dark-mode-controller" class="link mt-8 mb-6">

And don’t for­get to ini­tial­ize Stimulus

import { Application } from 'stimulus'

import DarkModeController from './controllers/dark_mode_toggle'

const application = Application.start()
application.register('dark-mode-controller', DarkModeController)

Image Optimization and Lazy-Loading

Turns out im­ages are whole thing. You can spend a lot of time cre­at­ing a sys­tem that op­ti­mizes and re-sizes your im­ages and makes sure that the right im­age di­men­sions are served to the right de­vices. Eleventy even has an of­fi­cial plu­gin to do this.

In this re­gard my goals were a bit dif­fer­ent:

Given these goals, what I’m about to de­scribe may not be the ul­ti­mate cut­ting edge best prac­tice, but it works for me. The first and eas­i­est step is to make sure your im­ages are op­ti­mized and com­pressed, even if they are the same di­men­sions when they come out the other end. For this, I used par­cel-plu­gin-im­agemin which al­lows ParcelJS to ap­ply those op­ti­miza­tions dur­ing the build. I found that this alone re­duced the size of my im­ages by 80% for free.

7:39:10 PM: \$ cross-env NODE\*ENV=production parcel build ./src/assets/\*\*/\_ --out-dir ./dist/assets --no-source-maps
7:39:11 PM:
7:39:34 PM: ✨ Built in 23.57s.
7:39:34 PM: dist/assets/img/yelp-search-home.png 432.99 KB 22.31s
7:39:34 PM: dist/assets/img/codenail-framed-poster.jpeg 347.73 KB 18.52s
7:39:34 PM: dist/assets/img/yelp-search-ios.jpg 186.6 KB 16.85s
7:39:34 PM: dist/assets/img/yelp-nearby-ios.jpg 156.13 KB 16.42s
7:39:34 PM: dist/assets/js/app.js 80.55 KB 21.05s
7:39:34 PM: dist/assets/img/instant-kitty.png 42.58 KB 3.23s
7:39:34 PM: dist/assets/img/renderproxy-landing.png 32.99 KB 13.61s
7:39:34 PM: dist/assets/img/instant-dai.png 22.86 KB 2.92s
7:39:34 PM: dist/assets/img/0x-api-big.png 22.62 KB 13.54s
7:39:34 PM: dist/assets/img/instant-rep.png 21.34 KB 3.02s
7:39:34 PM: dist/assets/img/0x-api-banner.png 11.96 KB 3.32s
7:39:34 PM: dist/assets/css/app.css 8.47 KB 18.61s
7:39:34 PM: dist/assets/img/renderproxy.png 4.85 KB 3.02s
7:39:34 PM: dist/assets/img/0x-api.png 4.67 KB 1.42s
7:39:34 PM: dist/assets/css/prism.css 2.32 KB 18.62s
7:39:34 PM: dist/assets/img/codenail.png 1.56 KB 933ms
7:39:34 PM: dist/assets/img/yelp.png 1.03 KB 1.14s
7:39:34 PM: dist/assets/img/0x.svg 887 B 737ms
7:39:34 PM: dist/assets/img/just-a-level.png 391 B 926ms

The sec­ond step I took was to use eleventy-plu­gin-lazy­im­ages. This plu­gin scans your markup for <img> tags, seeds them with in­line low-res im­ages, and loads the full res­o­lu­tion im­ages once the im­age is near the view­port. The re­sult is HTML like the fol­low­ing:

alt="My Alt Title"

I found that us­ing these two plu­g­ins, along with be­ing smart about which im­ages I use, was Good Enough™ for me.

Markdown Plugins

At some point while work­ing with mark­down files in Eleventy you may find that you want some cus­tom be­hav­ior. Fortunately, this is sup­ported, as Eleventy al­lows you to add and con­fig­ure your own markdown-it in­stance.

While there may be a lot of ways you want to con­fig­ure your mark­down parser, I’m go­ing to go over how I im­ple­mented perma-links for my head­ings, and how I au­to­mat­i­cally made ex­ter­nal links open in a new tab.

Perma-links, which you can check out by hov­er­ing over any head­ing, were added by us­ing mark­down-it-an­chor and the link” icon from feather-icons. Simply en­able the permalink op­tion and set the permalinkSymbol to the icon of your choice.

To en­sure ex­ter­nal links open in a new tab, I used mark­down-it-link-at­trib­utes, which lets you ap­ply at­trib­utes to links meet­ing a cer­tain cri­te­ria. In my case, I made sure to add target=_blank and rel= noopener noreferrer to all links with ab­solute URLs, since I only use rel­a­tive URLs for in­ter­nal links.

* Set markdown libraries
* @link

markdownIt({ html: true })
.use(markdownItAnchor, {
permalink: true,
permalinkSymbol: '<i data-feather="link" class="link"></i>',
.use(markdownItLinkAttr, {
// Make external links open in a new tab.
pattern: /^https?:\/\//,
attrs: {
target: '_blank',
rel: 'noopener noreferrer',

Email Updates Using RSS

If you look at the bot­tom of this page, you’ll no­tice a sub­scrip­tion form. This fea­ture is im­ple­mented by us­ing a com­bi­na­tion of eleventy-plu­gin-rss to au­to­mat­i­cally gen­er­ate an RSS feed.xml file and Mailchimp.

Using the plu­gin, you can eas­ily add RSS to your site by adding a feed.njk file to the root of your blog. Here is mine:

"permalink": "feed.xml",
"eleventyExcludeFromCollections": true,
"metadata": {
"title": "fragosti",
"description": "Posts, projects and more by fragosti",
"url": "",
"feedUrl": "",
"author": {
"name": "Francesco Agosti",
"email": ""
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="">
<title>{{ metadata.title }}</title>
<subtitle>{{ metadata.description }}</subtitle>
<link href="{{ metadata.feedUrl }}" rel="self"/>
<link href="{{ metadata.url }}"/>
<updated>{{ | rssLastUpdatedDate }}</updated>
<id>{{ metadata.url }}</id>
<name>{{ }}</name>
<email>{{ }}</email>
{%- for post in %}
{% set absolutePostUrl %}{{ post.url | url | absoluteUrl(metadata.url) }}{% endset %}
<title>{{ }}</title>
<link href="{{ absolutePostUrl }}"/>
<updated>{{ | rssDate }}</updated>
<id>{{ absolutePostUrl }}</id>
<description>{{ }}</description>
<content type="html">{{ post.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }}</content>
{%- endfor %}

Once you have that, Mailchimp and other mar­ket­ing plat­forms of­fer ser­vices that will con­vert up­dates to your RSS feed to email up­dates for your sub­scribers. This is nice be­cause you don’t have to cross-post, or write a com­plex in­te­gra­tion every time you want to re­lease a post. The only thing you need to im­ple­ment is the email it­self, and for that Mailchimp pro­vides RSS Merge Tags, mean­ing you can eas­ily in­clude your blog ti­tle and con­tent in your emails. As soon as you write a new blog post and de­ploy it, Mailchimp will pick it up and send out the email.


As you can see Eleventy pro­vides a sim­ple and el­e­gant so­lu­tion for those want­ing to build a sta­tic blog. It’s easy to get started, and in my ex­pe­ri­ence, it pro­vided so­lu­tions to all prob­lems I wanted to solve while de­vel­op­ing, ei­ther in the form of doc­u­men­ta­tion or a plu­gin.

I’m sure things will con­tinue to evolve, but I hope this post is help­ful to peo­ple im­ple­ment­ing fea­tures that aren’t ex­actly cov­ered by the main Eleventy docs.