Skip to main content
Metaist

Adventures in 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.

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 default liquid template language?

      • Update 1: I need to change set to capture; remove | safe for content, since it's already escaped; and convert var1 or var2 expressions to var1 | default: var2.
      • Update 2: See below, when I change it all back.
    • 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 an eleventyNavigation 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

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

Updates #

2023-05-18 #