Blog Posts
My static site stack - 11ty
In the beginning all sites were static. Then came CGI, or the Common Gateway Interface, and the application servers for PHP, Python etc that followed, to render dynamic pages per request on the server, sending the client HTML.
As Javascript became more powerful, and applications more complex, ever more sophisticated client tools were developed to move the dynamic content from server to client. The current status quo is reactive frameworks like React, Angular, Vue etc, which come at a cost of hundreds of Javascript dependencies. The result is complex applications than run largely on the user's browser. Npm manages the dependencies and tools like Webpack and Vite reduce the amount of code sent over the wire.
I have nothing against reactive frameworks - see my post on my use of Sveltekit for web application development.
However, this site is just text, images and links, and I wanted something that focussed purely on delivering these, allowing me to write content without getting bogged down by formatting and framework.
Why not Wordpress?
Wordpress is a very popular blogging platform. In fact, it is a popular platform period. Googling it will tell you that 43% of all web sites, and 63% of web sites served by a CMS (Content Management System) are served by Wordpress.
So why don't I use it?
In Wordpress, you enter your content through its GUI. It is saved in a database and when the user views the pages, they are fetched by the backend PHP code and rendered.
There are a number of issues with this, some of which Wordpress has addressed, others not. The biggest issue for me is, as a developer, I like my content in Git, where it is versioned, where I can revert, and where I can test on my laptop or a test server before committing to production, etc. You can't do this with Wordpress. If you did try, with a seprate instance and its own database, and wrote tools to copy from one to the other, the tooling complexity would seriously reduce your confidence that production matches development.
The other problem is it is PHP. Ultimately I would end up tweaking the platform, and I had enough of PHP in the 1990s. There have been better languages around for decades.
Why not just write HTML?
Yeah, I could just write HTML pages. If I put enough effort in, I could reasonably separate content from styling. I would have to write links myself - blog indexes, next and previous links, etc, or write code to parse my content.
I like Markdown though. I write documentation in Markdown. I want write my blog in Markdown. Markdown renderers do a pretty good job at making them look decent, especially code fragments. This is a coding blog so code fragments are especially important.
I'm sure there are components to convert Markdown to HTML I could use, and I could write something to parse files for links and create indexes. Fortunately it turns out this is a solved problem. Enter Static Site Generators, or SSGs.
Static site generators
SSGs have been around for a while. I believe the first that could reasonably be put in this category was Jekyll, which has been around since 2008. The idea is you write you write your content in (an extended) Markdown. Then you write layout files and index pages that can iterate over your posts, and CSS files to give them styling. Jekyll takes these and outputs static HTML. Lovely.
As it's such a good idea, and quite mature, obviously there are alternatives. Jekyll is written in Ruby, so that's the language you extend it in. Don't like Ruby? There's Hugo, written in Go.
I don't like Ruby or Go. For web tools, Javascript is hard to beat because of all the packages NPM opens up. As this is the 2020s, the are also Javascript SSGs. Most of these are really layers on top of the reactive frameworks I was trying to avoid in the first place. Gatsby uses React as its template language. The popular reactive fullstack frameworks such as Next.js and Nuxt.js can (apparently) be connfigured to write purely static code. But rather than get bogged down by complex tooling for a very uncomplex blog, I just chose the simplest Javascript-based SSG I could find - 11ty. Even the name is small.
11ty
11ty, or Eleventy - I never quite know which is the preferred styling - is based on the same concept as Jekyll. With some key differences:
- It is written in Javascript, installed with NPM, run with Node.
- It supports 11 markup languages, not just Markdown, including Nunjucks, Liquid, and others. And these can be mixed and matched.
- It is fast. Jekyll is fast compared to reactive frameworks, but 11ty is fast, period.
Truly static
My 11ty-built site is truly static. Have a look at the code. It is all HTML and CSS. Didn't get asked if I can send you cookies? That's because I don't send any. GDPR compliant? Yes, because I don't collect your data.
I don't have an application server. It is hosted with Apache. I don't even use Docker. Security is simple because I have nothing except a bare-bones Ubuntu and bare-bones Apache.
I do have one bit of Javascript, for my analytics provider of choice, Plausible. Plausible is privacy-first, doesn't use cookies, doesn't store personal data, it's owned and hosted in the EU. It's tiny - I think the Javascript is 3K. That's my one compromise.
Setting up 11ty
If you're familiar with NPM and Node-based tooling, setting up 11ty is as intuitive as it gets:
mkdir myproject
cd myproject
npm init -y
npm install @11ty/eleventy --save-dev
Make a markdown file, eg index.md
:
# This is my post
This is a post written with [Markdown](https://www.markdownguide.org/). All its features such
as *italic* and **bold** are supported by 11ty.
and run 11ty:
$ npx @11ty/eleventy
[11ty] Writing _site/index.html from ./index.md (liquid)
[11ty] Wrote 1 file in 0.11 seconds (v2.0.1)
Your site is now in the _site
directory. It has a single file, index.html
. Can't get simpler than that.
Like all good Node development tools, it has a dev server which watches for changes and automatically runs 11ty. Edit the package.json
:
@@ -7 +7,2 @@
- "test": "echo \"Error: no test specified\" && exit 1"
+ "build": "npx eleventy",
+ "dev": "npx eleventy --serve"
then run
npm run dev
Layouts
Layout files wrap your content files in boilderplate HTML. They are let you include metadata. Change index.html
to
---
title: This is my post
layout: base
---
This is a post written with [Markdown](https://www.markdownguide.org/). All its features such
as *italic* and **bold** are supported by 11ty.
Create a directory called _includes
and, inside that, a file called base.njk
(using Nunjucks this time):
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ title }}</title>
</head>
<body>
<h1>My static site stack - 11ty</h1>
{{ content | safe }}
</body>
</html>
Styling
Create a directory called css
and a file within it called style.css
:
body {
font-family: "Open Sans", sans-serif;
margin: 10px;
}
Edit your base.njk
layout to include it:
@@ -4,2 +4,3 @@
<title>My static site stack - 11ty</title>
+ <link rel="stylesheet" href="/css/style.css" />
</head>
11ty doesn't process CSS files. So that it includes them anyway, we have to tell it about them. Create a file .eleventy.js
:
module.exports = function(eleventyConfig) {
eleventyConfig.addPassthroughCopy("css/style.css");
}
Now style.css
is applied to all our pages.
Collections
If you add
tags: post
to your content files then you can iterate over all files with that tag.
Create the following in your project directory:
|-- .eleventy.js
|-- _includes
| |-- base.njk
|-- blog
| |-- firstpost.md
| |-- secondpost.md
|-- css
| |-- style.css
|-- index.md
|-- package.json
firstpost.md
:
---
title: My first post
layout: base
tags: post
---
This is my first blog entry
secondpost.md
:
---
title: My first post
layout: base
tags: post
---
This is my second blog entry
index.md
:
---
title: Welcome to my blog
layout: base
---
<ul>
{% for post in collections.post %}
<li><a href="{{ post.url }}">{{ post.data.title }}</a>
{% endfor %}
</ul>
You get the idea. The rest is just styling and customisation. Iteration is in inceasing order by date created by default. This can be reversed. If you need other filters, like my first-twenty only filter, you can write them in Javascript in your .eleventy.js
More documentation is in 11ty's getting started guide.
Syntax highlighting
11ty has plugins, some from the official developers, others third party. My favourite is the syntax highlighting plugin, based on Prism. Just run
npm install @11ty/eleventy-plugin-syntaxhighlight --save-dev
As you can see from the code fragments above and my other posts, Prism supports a variety of languages. It is also stupidly simple to customize. Here is how I highlight the directory structure above.
In my .eleventy.js
:
const Prism = require('prismjs');
// ...
module.exports = function(eleventyConfig) {
Prism.languages.folders = {
// Just highlight symbols used to denote folder structure
keyword: /^([-|+`\s]+)/gm
};
Tailwind
As I've said before, I like TailwindCSS. Thanks to 11ty being Javascript, I can use this too.
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init
In postcss.config.js
:
module.exports = {
plugins: {
'postcss-import': {},
tailwindcss: {},
autoprefixer: {},
},
};
In css/tailwind.css
:
@import "tailwindcss/base";
@import "./base.css";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
This lets me define my own classes in my base.css
, using postcss , eg:
body {
@apply antialiased text-zinc-300 bg-zinc-800;
font-family: 'Sora', sans-serif;
}
To use the Tailwind styles, simple include tailwind.css
in your base.njk
:
@@ -5 +5 @@
- <link rel="stylesheet" href="/css/style.css">
+ <link rel="stylesheet" href="/css/tailwind.css">
As Tailwind relies on PostCSS to dynamically create its classes, we need to run PostCSS during the build process. We also need to tell Tailwind what file extensions to parse. In tailwind.config.js
:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./**/*.{njk,md,css,html}", "./**/*.svg",],
theme: {
extend: {},
},
plugins: [],
}
We can transparently build both PostCSS and 11tpy, and run both dev servers concurrently, with the very handy npm-run-all
. Install it with
npm install npm-run-all --save-dev
Then edit package.json
:
@@ -7,2 +7,6 @@
- "build": "npx eleventy",
- "dev": "npx eleventy --serve"
+ "dev": "npm-run-all -p dev:*",
+ "build": "run-s build:*",
+ "dev:11ty": "eleventy --serve",
+ "dev:css": "tailwindcss -i css/tailwind.css -o _site/css/tailwind.css --watch --postcss",
+ "build:11ty": "eleventy",
+ "build:css": "tailwindcss -i css/tailwind.css -o _site/css/tailwind.css --postcss"
Now npm run build
and npm run dev
will run both elevnty and tailwindcss automatically.
Summary
So you can see my basic site is not that basic. And I haven't gone through its other features, such as a more structured directory layout, date formatting, sitemaps.xml
and robots.txt
generation, SEO features. I can use anything that NPM puts at my disposal, yet my site is pure HTML and CSS and production is just Apache.
There are lots of static site generators out there. Others support more complex and interactive site - Javascript hydration, code splitting and other modern patterns but for me a simple HTML and CSS site is what I want. Decide the language you like, how much additional interactivity you want to provide, and pick the SSG that best suits your needs.