A shabby hook for an Eleventy gallery
— published on 21·08·25In anything that has to do with computers, as with many other fields, a general rule of thumb to keep in mind is: If it's simple, it's because it's complex. If it's so simple to log into your computer with three clicks and open your inbox in under a minute, it's because someone made complex decisions in your place. And in the case of Eleventy, rated one of the simplest static site generators to use, if you can get a full blown blog or documentation website rolling in under an hour, it's because there's so many little moving parts, that your workflow seems seamless (excuse the bad alliteration).
Eleventy can be a wonderfully easy way to create a website, unless you've got a case of the Charlie Chaplins and you can't keep your hands off the gears. I got to experience this firsthand when I started rebuilding my design portfolio. I've been using Eleventy for a few years now, and it hasn't let me down. But there was a tough nut to crack: The only way to really "group" content is with the "collections" feature, and it's mainly made for... pages.
Eleventy's handling of images
What I need, and what I imagine many people who want a portfolio need, is a clever way to make an image gallery for each project. This means that for each project page, a group of images should be shown somewhere on the page. As I'm building a portfolio, text is not a must-have thing. Images, specifically groups of related images take precedence and priority.
Option one: Images as the page's content
One way to do this is to put the images in a folder, then link to each image one by one inside the post. Eleventy uses the Markdown syntax to write the main content, so it would look like this:  over and over, manually written and repeated for each image. And while this takes the dexterity of a four year old and the effort of a first-grader, it's not ideal for two reasons: First, it's an annoying repetitive task that takes more time than it's worth. Second, this makes your images part of your written content, without much else to set them apart or group them, as Markdown doesn't have syntax for grouping. You can't style or tweak the structure of the group of images as a whole, and this might defeat the idea of a "gallery". You could in theory inject some JavaScript to do the trick after the HTML is generated, and you can img:nth-of-type(n) the crap out of them, but that's neither seamless nor intuitive nor easy.
Option two: Images in the metadata
Eleventy makes use of YAML metadata frontmatter at the top of Markdown files, which provides context to a text. The Nunjucks templating engine can read those values as it populates, i.e. puts page content, inside the templates. Just like you have the frontmatter properties title and tags, you could add an images property with a list of paths to your images. The templating engine (and the templating engine alone) can pick this up and use it like so:
<div class="gallery>
{% for image in page.images %}
<img src="{{ image }}>
{% endfor %}
</div>
This is arguably better, as it allows you to group the images into a specific element and style them separately from the content. But as your projects grow in number and in images, it becomes an annoying task as repetitive as the solution above, as you still have to write everything by hand all the time, alongside adding your image files to a directory. If your website is a portfolio, that might be the main thing you'll do every time you update it.
The ideal solution should require you only to create the project page with YAML frontmatter, add text if you wish, and add the images to a folder named like the project.
Welcome to the machine: frustrations of a Node-newbie
So, Eleventy doesn't have a clear and accessible way of automatically displaying all images related to a blog post. Enter the gears, and bring your multitool. I did.
After searching for a while, I remembered Eleventy has a handy thing called directory data files. It allows me to, drumroll, set common data to be used inside templates when building the individual pages inside a, drumroll, directory. With a bit of head-bashing tinkering, I figured out how to make the data file output both handwritten data like common template, tags, and author, and computed data through a function. So I went ahead with this:
// Import the filesystem and path Node.js modules
let fs = import("fs");
let path = import("path");
export default function () { // The one and only function allowed in the file
let imgFolder = path.join('/images', page.fileSlug); // From Eleventy's Supplied Data, get the slug of the page to search for a matching directory
let imageList = fs.readdir(imgFolder); // List all the files in a directory matching the slug, assuming they are images
return {
tags: [
"posts"
],
"layout": "layouts/post.njk",
"pageImages": imageList,
};
}
I thought I would call it a day. Except for Error: page is not defined. Apparently, the one object that would always make sense to have around needs to be manually added as an argument. So I changed line 4 to: export default function (page) {.
11ty figures out what I mean, but now path.join is not a function. Except of course it is, it's always been. But it seems importing the path module through import() only imports the Promise (literally its name) of a module named this way, and not the module itself. As Node.js is asynchroneous by default, it doesn't wait for the module to actually be loaded before running the rest of the code. On we go to write import path from "path" for a static import. Now that's good and read, but Eleventy can't for the life of it understand what fileSlug is because the individual page slugs are not computed yet. As said above, only the templating engine can access the pages metadata. The only option is to wrap the return statement around an eleventyComputed object which supposedly waits for the individual pages to be read. How in the hell I am supposed to do that properly I could not figure out.
It would make sense that an SSG with an otherwise super easy and welcoming way of working, especially with regards to blogging, would have an automatic per-page gallery feature. I can't possibly be the only one using it to make a portfolio, right? I looked for a solution, this time deep within myself as search engines are an unusable hell now and even their AI counterparts don't understand.
Listing the menu before cooking the meal
The only logical solution which wouldn't compromise my sanity, burn out my time, and mess up my website comes in the form of a pre-baked sheet of data that templates can read made from listing the image files. In human language, this means that a program should launch before Eleventy starts, read the files inside my content directory, match them with existing image directories, and list the images inside the latter. Then it should output all of that to an easy to parse data file, go to the pub, have a pint, and wait for all this to blow over.
The POSIX terminal rat way
If you're as handy with shell scripting as the friend to whom I voiced my concerns, you might write a script that does all that in the shell. After all, it's basic string and directory listing operations. Then you could launch it every time before launching Eleventy, and the data files should be ready to roll.
But because I'd rather invest time in learning Node than bashing my head against POSIX compliant cheatsheets for this particular project (an activity which I otherwhise wholeheartedly recommend), and because the whole thing is already written in JavaScript, I'll go with what's already on the menu.
Eleven hooks and splices
Yes, that is both a pun on Eleventy, KFC, and JS methods.
In its configuration file, Eleventy proposes a handy way to do custom things at different stages of its build process. When eleventy runs, it triggers a few events by default, which you can respond to with custom code called an event handler. Had I learned Node.js earlier, I could have just called a package inside this event handler. Instead I know default JavaScript, so wrote the entire process inside the handler.
Prerequisite
Something I shouldn't omit is that this particular script was written with my own directory setup in mind. Here are my blog files. If you don't set a custom fileSlug for your pages, it should work.
11ty-project/content/blog/
├── blog.11tydata.js
├── fourthpost.md
├── firstpost.md
├── secondpost.md
└── thirdpost.md
Mirroring this, I have a directory with subdirectories corresponding to the filenames of the posts, excluding the .md.
11typroject/public/img/
├── firstpost
│ ├── animage.png
│ ├── image2.png
│ └── drawing.png
└── fourthpost
├── hci.png
└── screenshot.png
It's important to note that if inside the Markdown files, you set the slug to a different value, or change in any way the resulting page.fileSlug value without reflecting this in the images' directory names, this method won't work as it relies on Node.js scanning the file system. If you haven't touched anything, it should work by default as long as your images structure resembles this one.
The meat and potatoes
The piece of code works in two steps. First, we write the event handler and we scan for images in our directories.
eleventyConfig.on("eleventy.before", async ({ directories, runMode, outputMode }) => {
console.log('Starting images scan');
// Paths are relative to eleventy.config.js directory
const imagesDirPath = 'public/img/';
// Initialize the array of objects with the data
const imgsObj = [];
// Read the directory for subdirectories, and read each for files
const imgPostsArr = fs.readdirSync(imagesDirPath,(err,files)=>{});
imgPostsArr.forEach((imgPost) => {
const imagesList = fs.readdirSync(path.join(imagesDirPath, imgPost),(err,imgs)=>{});
// Push the data to our array of objects
imgsObj.push({
postName: imgPost,
postImages: imagesList
});
});
The first line is just how 11ty suggests we do things. I don't use the arguments in any way, but I'll keep them here just in case. Then we tell 11ty where to look for image directories, and how to store the information. And the major thing then is reading the contents of our general image folder, which returns a list of subfolders we loop through to read them as well and return the files inside. For every subfolder, we add an object to our array, including the post's name and the list of images. Later on, during the templating phase, Eleventy will match our articles by their slug name against the file, and receive a list of images to loop through.
We first have to store that data on file. Just to be sure, let's add console.log(imgsObj); under all that and before the next part:
// Check whether to update or create the file
if (fs.existsSync('_data/dataFileTest.json')) {
// Only write to file if something is updated, else during watch it will loop forever
var jsonFileContents = fs.readFileSync('_data/dataFileTest.json', 'utf8');
// Next line is black magic. I can't make it work if I don't stringify both outputs
if (JSON.stringify(jsonImgsObj) == JSON.stringify(JSON.parse(jsonFileContents))) {
console.log("No images data changed");
} else {
// There is new data? update the file
fs.writeFileSync('_data/dataFileTest.json', JSON.stringify(jsonImgsObj));
console.log("Updated image data");
}
} else {
// The file doesn't exist? Create it!
fs.writeFileSync('_data/dataFileTest.json', JSON.stringify(jsonImgsObj));
console.log("Updated image data");
}
// Close our event handler
});
The file in question can be named any way you want (as long as it matches what's in the code and in the template later), and is a global data file residing in _data/, a file that exists for all page and all stages of our build. If we don't check whether we really need to update the data before writing to file, 11ty will run this code, create the file, see that a file in its workign directory has been changed, and re-run the same code. It will do that infinitely. So first we need to verify if the file exists. If it doesn't, create it. If it does, check if the data we just calculated is different from the data in the file. If it isn't, do nothing. If it is, update the file with the new data.
This entire thing should be pasted together, somewhere in the default eleventyConfig function. We now have a thing that creates a file with data. Here's the contents:
[
{
postName: 'firstpost',
postImages: [ 'animage.png', 'image2.png', 'drawing.png' ]
},
{
postName: 'fourthpost',
postImages: [ 'hci.png', 'screenshot.png' ]
}
]
You can later edit the data in the file and add properties like alt-text for accessibility for example. This is hard to do with a simple script.
Templating
What makes data files powerful is that they're accessible by templates. Well, they're accessible, not accessed by default. So let's make sure we receive the info:
<div class="gallery">
{% for article in dataFileTest %}
{% if article.postName == page.fileSlug %}
{% for image in article.postImages %}
<img src="/img/{{ page.fileSlug }}/{{ image }}">
{% endfor %}
{% endif %}
{% endfor %}
</div>
Inside an element called "gallery" (or call it however you want, I'm not your dad) we make Nunjucks loop through the array inside the JSON file. That file is accessed by its filename minus the extension. If the object inside the array has a postName matching the slugname of the current post being populated inside the template, loop through its images. For each, create an image element linking to the built image folder, to the folder matching its file slug, to the image's filename itself.
We're done. We now have a neat gallery element separate from the page's main content. We can style it however we want and separate it from the images inside the content and article.
Feedback
If you have any thoughts or comments about this site or about an article, send me an email!
If you like what you just read,