The Darth Mall a personal website

A Subtle Footgun in Eleventy’s Default Permalink Algorithm

Published
Updated
Tagged
Eleventy

Let’s suppose we have a blog built with Eleventy. And let’s further suppose that we want our URLs for our blog posts to be based on the publication date for the post; something like /blog/<year>/<month>/<day>/. Seems perfectly reasonable, right? There’s a footgun1 here. Any time we publish on a day whose date matches the month — 1 January, 2 February, 3 March, etc. — the <day> part of our URL is going to be stripped out and we’ll be left with /blog/<year>/<month>/.

The footgun

My mental model of how Eleventy maps input files to output files by default is that the path of the file in the input directory is the path of the file in the output directory. If I have a blog post written on 1 August in my input directory at blog/2025/08/01.md, I expect that file to be written to blog/2025/08/01/index.html in my output folder. And for the most part this is true.

There is a special case, however, for when the file name matches the directory name. In that case, the file name is ignored. So a post written on 8 August stored at blog/2025/08/08.md ends up being written to blog/2025/08/index.html. Note how it’s missing the second 08/ directory in the output path.

This is, to me, a pretty surprising special case.

The workaround

Thanks to Eleventy’s dynamic permalink key we could put our blog posts pretty much anywhere we want and still write them out to our desired blog/<year>/<month>/<day>/index.html output path. But let’s suppose we want to keep the mapping between the input directory and the output directory so that it’s obvious just from the file system where everything ends up.

We can write a short permalink function in a directory data file in our blog/ directory so that it applies to all input files in that directory. It will be called blog.11tydata.js and it will look like this:

export default {
permalink: function ({ page }) {
if (path.filePathStem.endsWith("index")) {
return `${page.filePathStem}.html`;
}

return `${page.filePathStem}/index.html`;
}
}
blog/blog.11tydata.js

This example uses ECMAScript modules (ESM). If you’re using CommonJS (CJS) modules, your export will look different. If you’re not sure what any of that means, you can join the Eleventy Discord server and ask for help. Someone will be able to get you sorted.

First thing we do is check to see if the file for which we are generating a permalink is already named “index” — index.md, index.liquid, you get the idea. If it is, we append the .html extension to the filePathStem that Eleventy provides us (because the filePathStem already ends with index). This preserves Eleventy’s default treatment of files named “index”. If the file is not named “index”, we append /index.html to the filePathStem to ensure our URLs all have a trailing slash.

Another footgun I discovered while solving this problem is that when you have an input file named “index”, the Eleventy-supplied page.fileSlug is not index as you might expect, it is instead the name of the parent directory. This is why the test in our permalink function checks to see if page.filePathStem ends with the text index instead of checking to see if page.fileSlug equals index. And, in case you were wondering, if the file is named “index” and has no parent subdirectory because it’s in the root of your input directory, page.fileSlug is empty. ¯\_(ツ)_/¯ All of this (and more) is documented, but honestly…

The opinion

As a general rule, I dislike special cases. They make it harder to learn how a system behaves. What could be a simple explanation becomes riddled with exceptions; “this behaves this way, except when…” And every one of those exceptions is an additional thing you have to memorize and keep in your head when you’re trying to reason about how a system is going to behave.

Sometimes special cases are warranted. This, however, seems like a weird and unnecessary special case to me. There are three other methods for generating an index.html for a subdirectory that (arguably) don’t break with Eleventy’s convention of mapping the input file structure to the output file structure.

  1. Create your subdirectory, subdir/, and then create a file that sits next to it (not inside of it) with a matching name, subdir.md
  2. Create an index template file: subdir/index.md
  3. Create any file anywhere in your input directory and override the permalink to be subdir/index.html

You could argue that the second option also violates the convention of mapping input file structure to output file structure because, if we stuck with that literally we would end up with subdir/index/index.html in our output. This seems like a more intuitive break with the convention, at least for people who are already familiar with how index.html files behave in a web server. But, honestly, now that I look at it, I’d be pretty ok with dropping this one, too.

Think about it. Currently, the explanation of how Eleventy decides where to put an input file (at least as far as I’ve discussed here, there are actually a few more rules) is:

Write an index.html file to a directory whose path matches the path of the input file, except if a permalink is set for that file, or if the file is named “index”, or if the file name matches the directory name.

That’s three exceptions to the rule. Now it is useful not to be completely tethered to your file system structure for your website structure, so an exception probably is warranted. Eleventy’s permalink is quite powerful, so we’d probably all be just fine were that the only exception. In which case the explanation would read:

Write an index.html file to a directory whose path matches the path of the input file, except if a permalink is set for that file.

Ah, that’s much easier to explain.

And even after eliminating the exceptions for “index” templates and templates-whose-names-match-the-parent-directory, there are still two ways to create a subdir/index.html file: setting permalink or creating a subdir.md template as a sibling of the subdir/ directory.

Anyway, I opened an issue. I had probably read about how Eleventy handles templates-whose-names-match-the-parent-directory half a dozen times in the docs and it never really occurred to me that there was a footgun there. But since a friend ran into this actual problem using what is, in my opinion, an eminently reasonable directory structure for her blog (the date-based one we’ve been using as an example), I kinda think this behavior should be reconsidered. Weigh in if you have a GitHub account and an opinion.

Or don’t. I’m not your dad.


Footnotes

  1. A footgun is something with which you shoot yourself in the foot. ↩︎