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. ↩︎