Adventures in Eleventy
Long post tracing my attempt to convert my blog to eleventy.
Previously: 2023-05-14 Weekend Notes
Contents
Goal #
To migrate my old blog posts to eleventy
. If that doesn't work, my next goal is to simply have a basic site with posts, tags, and RSS feed.
Background #
I started this blog in 2004 on blogger as part of an school assignment. In 2009, I deleted all the posts and started again as a place for my thoughts. In 2011, I decided to move all my content over to my own domain and I built a backwards-compatible version of the site using PHP (here's my post at the time). Then at the end of 2012, I wrote my own python static site generator called blogit
.
Writing consistently has been difficult for me. I kept setting the bar too high. Each blog post needed a catchy thumbnail. I'd obsess and reword all the text hundreds of times and change the tags obsessively (even now I'm rewording this paragraph quite a bit). I've mitigated it a bit by having a monthly newsletter, but my public output has dropped to near-zero.
Also: blogit
was kinda slow. I started thinking about ways I could cache the data to generate the website faster. But this was all dumb. I should have just used jekyll
or hyde
and been happy. But I was stubborn.
Fast-forward a decade and building my own static site generator seems like a poor use of my time (no matter how clever). Shalev NessAiver recently pointed me at Simon Willison’s Weblog and his style of posts inspired me to try again and over this past the weekend, I discovered eleventy
which looks like it has sensible defaults and fast build times. I'm game.
.markdown
vs .md
#
Usually I like shorter file extensions, but a decade ago it seems like .markdown
was unambiguous and more popular. Nowadays .md
is dominant and eleventy
assumes as much. I don't mind the opinionated approach, I just have to rename the files. Ah, but in blogit
I had made the file modification time be the post's updated time. If I rename all the files, I need to preserve the modification time.
for f in *.markdown; do
cp -p $f # preserve times
rm $f
done
Update (Take 2): At first, I kept the .markdown
file name so that I can use it as a different template format. I'll deal with the <published>
and <updated>
fields another time.
// eleventy.config.js
const showdown = require("showdown");
// ...
module.exports = function (eleventyConfig) {
// ...
const showdownConverter = new showdown.Converter();
const showdownRender = (text) => showdownConverter.makeHtml(text);
eleventyConfig.addTemplateFormats("markdown");
eleventyConfig.addExtension("markdown", {
outputFileExtension: "html",
compile: async (inputContent) => {
const result = showdownRender(inputContent);
return async () => result;
},
});
// ...
};
But then, after I figured out how to fix markdown in HTML (see below), I just made .markdown
and alias for .md
:
eleventyConfig.addExtension("markdown", { key: "md" });
YAML
front-matter #
The first problem is that back when I wrote blogit
, YAML front-matter didn't have a clear second delimiter. You could use ---
to start a new document or ...
to continue the stream. Looks like I chose wrong; everyone seems to have settled on ---
.
Fine, what if I add configuration parameters to .eleventy.js
to change the delimiters:
eleventyConfig.setFrontMatterParsingOptions({
delims: ["---", "..."],
});
So that works for the old documents, but what about the new ones? Ok, forget it; I'll update all the old posts to use the ---
delimiter too.
Update (Take 2): Indeed, I did end up using ---
delimiters.
Permalinks #
In blogit
, I used {year}/{month}/{slug}.html
, but I kinda like the eleventy
approach of making permalinks customizable. The default is pretty sensible and abstracts away the mapping. I can just put the date in the file name if I want it there.
Update (Take 2): This was fairly straightforward:
// blog.11tydata.js
const path = require("node:path");
const makePermalink = (data) => {
const fileDate = path.basename(data.page.inputPath).substring(0, 10);
const yearMonth = `${fileDate.substring(0, 4)}/${fileDate.substring(5, 7)}`;
const slug = data.slug || data.page.fileSlug;
return `/blog/${yearMonth}/${slug}.html`;
};
module.exports = {
// ...
eleventyComputed: {
permalink: makePermalink,
},
};
Thumbnails #
In blogit
, I had a convention of using the post file name with a .jpg
or .png
extension to automatically map thumbnails to posts. I actually figured out how to get this to work, kinda, but then realized that the images weren't displaying because the images were in markdown inside of a <figure>
element (see next section).
Update (Take 2): I added another pair of curly braces around {thumbnail}
and added a computed data property:
// blog.11tydata.js
const { glob } = require("glob");
// ...
const findThumbnail = async (data) => {
const search = data.page.inputPath
.replace(/^\.\/content\/blog/, "./content/static/img")
.replace(/\.markdown$/, ".*");
const files = await glob(search);
return files.length ? files[0].replace(/^content/, "") : "";
};
module.exports = {
// ...
eleventyComputed: {
thumbnail: findThumbnail,
// ...
},
};
Markdown in HTML #
Back in the day, you could put markdown inside of an HTML block and simply add markdown="1"
to get it rendered. This was part of PHP Markdown Extra and Python-Markdown. But it seems like you'd have to overwrite the default markdown parser to get this to work with a different parser.
At this point I abandoned the idea of trying to port my old posts. Time to start fresh.
Update (Take 2): I tried to use showdown
to re-render the markdown.
const { EleventyRenderPlugin } = require("@11ty/eleventy");
const showdown = require("showdown");
const showdownConverter = new showdown.Converter({
ghCompatibleHeaderId: true,
tasklists: true,
});
const showdownRender = (text) => showdownConverter.makeHtml(text);
eleventyConfig.addTemplateFormats("markdown");
eleventyConfig.addExtension("markdown", {
outputFileExtension: "html",
compile: async function (inputContent, inputPath) {
return async (data) => {
const njk = await EleventyRenderPlugin.String(inputContent, "njk");
const njk_rendered = await njk(data);
const md = await EleventyRenderPlugin.String(njk_rendered, "md");
const md_rendered = await md(data);
const result = showdownRender(md_rendered);
console.log(`[showdown] rendered ${inputPath}`);
return result;
};
},
});
But the actual solution was simpler: add a blank line after the opening tag and use the existing parser.
RSS #
Surprise! The out-of-the-box example of using the RSS plugin didn't work. But it seemed so simple! I'll come back to it later.
Update (Take 2): I got this to work and was able to preserve all the old <id>
tags:
// blog.11tydata.js
const path = require("node:path");
const crypto = require("node:crypto");
// ...
const makeID = (data) => {
const sha1 = crypto.createHash("sha1");
sha1.update(path.parse(data.page.inputPath).name);
return `tag:metaist.com,2010:blog.post-${sha1.digest("hex")}`;
};
// ...
module.exports = {
// ...
eleventyComputed: {
id: makeID,
// ...
},
};
LESS #
less
is still my favorite way to write CSS even after all these years. So I figure I could adapt the SASS example to use less
:
// eleventy.config.js
const less = require("less");
// ...
module.exports = function (eleventyConfig) {
// ...
eleventyConfig.addTemplateFormats("less");
eleventyConfig.addExtension("less", {
outputFileExtension: "css",
compile: async function (inputContent) {
return async (data) => {
const result = await less.render(inputContent);
return result.css;
};
},
});
};
This actually kinda worked except that eleventy --serve
only noticed the change in the .less
files, but didn't re-render them unless I touched the .eleventy.js
file itself.
Of course this just means I need to follow the "Registering Dependencies" section. Should work easily, right? Not quite. This will probably do the right thing if URLs within a .less
file change. Meh, will deal with this later.
Update: Turns out restarting eleventy
fixed this and now changes to the .less
file automatically re-render the page. Wonderful!
Highlighting? #
Bit of a reach, but could I add some basic highlighting? Worked correctly the first time. Impressive.
Using a Starter Repo #
So when I decided to start from scratch, I should have actually started from eleventy-base-blog.
-
Let's start with the blog metadata. Now that I know how how the data cascade works, this all makes sense.
-
Let's move on to the templates. Looks like there's a little helper for lists of posts. Might come back to this later.
-
The base template looks nice. Let's crib from here.
-
Q: How important is it to use the
.njk
rather than the defaultliquid
template language?- Update 1: I need to change
set
tocapture
; remove| safe
for content, since it's already escaped; and convertvar1 or var2
expressions tovar1 | default: var2
. - Update 2: See below, when I change it all back.
- Update 1: I need to change
-
Looks like I'll need to get the RSS plugin working correctly.
-
So that's how they do bundling. Not sure what I make of the claim that "Inlined CSS" has the "fastest site performance in production". I'll hold off on this for the moment.
-
The navigation code took me a second to process. I had to noticed that
collections.all
(every page on the site) was being piped through aneleventyNavigation
filter. This seems... wasteful? But maybe it's super fast? I'll need to remember to add the navigation plugin.
-
-
Updating the templates breaks things, so I'm going to jump to the plugins and get that setup. I'm surprised they're using
eleventy.config.js
instead of.eleventy.js
. Not sure it makes a difference. Gonna need a bunch of packages.
pnpm install --save-dev \
luxon markdown-it-anchor \
@11ty/eleventy-plugin-rss \
@11ty/eleventy-plugin-syntaxhighlight \
@11ty/eleventy-plugin-bundle \
@11ty/eleventy-navigation \
@11ty/eleventy-img
-
Ah, and I'll get to learn how to make custom plugins.
-
Oh, you can add your own watch targets. Very slick.
-
I'm not going to include these changes to the defaults.
- Update: I did end up including some of these.
-
So my styles are all messed up, so maybe I'll focus on how they do styles next. I'll need to work on the dark mode stuff later.
-
Ah, I see the benefit of moving things into a
content
folder. It doesn't pollute your top-level with unexpected bits. -
Just discovered that
nunjucks
is essentially a port ofjinja2
. I already know and like usingjinja2
, so I'm going to switch everything over.
Migration Take 2 #
Now that I managed to get a basic thing working, let me try, one more time to port my old blog.
- Tasks
-
-
- Wish List