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`;
}
}
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.
- Create your subdirectory, subdir/, and then create a file that sits next to it (not inside of it) with a matching name, subdir.md
- Create an index template file: subdir/index.md
- 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
A footgun is something with which you shoot yourself in the foot. ↩︎