---
title: Adding Image Galleries to My Website
description: At last, I've gotten around to implementing image galleries.
date: 2024-12-02
tags:
  - Site Updates
  - Eleventy
synopsis: At last, I've gotten around to implementing image galleries.
imageURL: /img/isabella-fischer-X2l9M6jsS7E-unsplash.webp
imageAlt: Some very tasty looking pop tarts with pink icing and sprinkles.
mastodon_id: "113586087349853099"
---
I've been meaning to add an image gallery feature to this website for ages, and I'm happy to finally announce that I've done it! If you'd like to see my very first image gallery in action, [here's a gallery of PopTart memes I've collected](../../gallery/pop-tart-flavor-memes/). If you're a massive nerd and would like to read about how I implemented this feature on my [Eleventy](https://www.11ty.dev/) website, read on.

Note: this is a fast and loose description of the process. It should be helpful if you're trying to do this yourself, but don't expect to be able to copy and paste my implementation. See [the repo](https://upchur.ch/gitea/n_u/nathanupchurch.com) to copy and paste my spaghetti.

## Specifying new galleries
First of all, I had to decide how new galleries would be specified so that Eleventy could work its magic. There are a few approaches here, such as utilizing template frontmatter, directories of images, or using a data file in my site's `_data` directory. After weighing up the pros and cons, I decided to use the latter option, even though it's not the most ergonomic. So I created `_data/galleries.js`, which looks like this[^1]:

``` javascript
export default [
	{
		title: "",
		description: "",
		synopsis: "",
		url: "",
		date: new Date(""),
		galleryImage: "",
		galleryImageAlt: "",
		pictures: [
			{
				title: "",
				filename: "",
				altText: "",
				thumbAltText: "",
				caption: "",
			},
		],
	},
];
```

## Gallery index pagination
Now that I know how I'll go about specifying my galleries, I need to paginate the gallery index pages; the pages that feature thumbnails of each image in the gallery and allow the user to navigate to the images. To do this, I created `content/galleries.njk`:
``` html
---{% raw %}
pagination:
	data: galleries
	size: 1
 	alias: gallery
layout: layouts/base.njk
tags: gallery
eleventyComputed:
	title: "{{ gallery.title }}"
	permalink: "/gallery/{{ gallery.title | slugify }}/"
	description: "{{ gallery.description }}"
---
<h1>{{ gallery.title }}</h1>
<p>{{ gallery.description }}</p>
<section>
	{% for picture in gallery.pictures %}
	<a href="/gallery/{{ gallery.title | slugify }}/{{ picture.filename | slugify }}/">
		<div>
			<img alt="{{ gallery.thumbAltText }}" class="gallery-image" src="{{ gallery.url }}{{ picture.filename }}">
		</div>
	</a>
	{% endfor %}{% endraw %}
</section>
```
By ensuring that each gallery index page is tagged "gallery," they will automatically be grouped into a new collection by that name, which is important so that I can filter galleries out from post lists and other things throughout my site.

## Generating image pages
I considered using a lightbox / modal sort of arrangement for viewing images, but I decided against it as it can be tricky to get accessibility right for these patterns, and I wanted to avoid using JavaScript if possible. What I chose to do instead was to create a page for each image, which would feature buttons to navigate to the next / previous image, and to return to the gallery. The first step was to create a collection containing all gallery images in my Eleventy config file:

``` javascript
// Collections
	eleventyConfig.addCollection("galleryImages", (collection) => {
		const galleries = collection.getAll()[0].data.galleries;
		let galleryImages = [];

		galleries.forEach((gallery) => {
			gallery.pictures.forEach((picture, i, arr) => {
				picture.containingGallery = `${gallery.title}`;
				picture.baseUrl = `${gallery.url}`;
				i ? (picture.previousImage = arr[i - 1].filename) : null;
				i + 1 != arr.length ? (picture.nextImage = arr[i + 1].filename) : null;
				galleryImages.push(picture);
			});
		});
```
You may notice that I did a little jiggery-pokery in that callback function to provide information about each image that isn't included in `_data/galleries.js` because it would have been a pain to include or would have unecessarily inflated the tile size, specifically: the filenames of images before and after the current image (should they exist), and the URL of the gallery that the image belongs to. We can use this information to have our image pages generated in the same directory as their parent gallery index, and to generate next / previous buttons to help the user navigate through the gallery. This information is added to the `galleryImages` collection object in memory so that we can use it when we paginate the image pages using `content/galleryImage.njk`:

``` html{% raw %}
---
pagination:
  data: collections.galleryImages
  size: 1
  alias: picture
layout: layouts/base.njk
eleventyComputed:
  title: "Image: {{ picture.title }}"
	permalink: "/gallery/{{ picture.containingGallery | slugify }}/{{ picture.filename | slugify }}/"
	description: "{{ picture.title }} from gallery: {{ picture.containingGallery}}"
---
<article>
	<h1>{{ picture.title }}</h1>
	<div>
		{% if picture.previousImage %}
		<a href="../{{ picture.previousImage | slugify }}">
			<button type="button">Previous</button>
		</a>
		{% endif %}
		<a href="/gallery/{{ picture.containingGallery | slugify }}/">
			<button type="button">Gallery</button>
		</a>
		{% if picture.nextImage %}
		<a href="../{{ picture.nextImage | slugify }}">
			<button type="button">Next</button>
		</a>
		{% endif %}
	</div>
	<figure>
		<a href="{{ picture.baseUrl }}/{{ picture.filename }}">
			<img src="{{ picture.baseUrl }}/{{ picture.filename }}" alt="{{ picture.altText }}">
		</a>
		{% if picture.caption %}
		<figcaption>
			{{ picture.caption }}
		</figcaption>
		{% endif %}
	</figure>
</article>{% endraw %}
```

Et voilà; we have galleries! But before we can call this project done, it would be nice to have a page that lists all galleries on the site for the benefit of visitors. For this, I created `/content/galleries/index.njk`:

``` html{% raw %}
---
layout: layouts/base.njk
eleventyNavigation:
  key: Pics
  order: 4
---
<h1>Image Galleries</h1>
<p>
Some pictures I thought would be worth posting.
</p>
<section>
<div>
	{% for gallery in galleries %}
	<article>
		<a href="../gallery/{{ gallery.title | slugify }}" class="postlist-link">
			<div>
				<img {% if gallery.galleryImage %} src="{{ gallery.url }}{{ gallery.galleryImage }}" alt="{{ gallery.galleryImageAlt }}" {% else %} src="{{ metadata.defaultPostImageURL }}" alt="{{ metadata.defaultPostImageAlt }}"{% endif %}>
			</div>
		</a>
		<div>
			<a href="../gallery/{{ gallery.title | slugify }}" class="postlist-link">
				<h3>
					{{ gallery.title }}
				</h3>
			</a>
			<time datetime="{{ gallery.date | htmlDateString}}">{{ gallery.date | readableDate("LLLL yyyy") }}</time>
			<p>{{ gallery.synopsis | truncate(105) | safe }}</p>
		</div>
	</article>
	{% endfor %}
</div>
</section>
```{% endraw %}

Note that `readableDate()` and `htmlDateString` are custom filters that came with the [Eleventy Base Blog template](https://github.com/11ty/eleventy-base-blog) that I based my website on. They require Luxon:
``` javascript
eleventyConfig.addFilter("readableDate", (dateObj, format, zone) => {
		// Formatting tokens for Luxon: https://moment.github.io/luxon/#/formatting?id=table-of-tokens
		return DateTime.fromJSDate(dateObj, { zone: zone || "utc" }).toFormat(
			format || "dd LLLL yyyy",
		);
	});

	eleventyConfig.addFilter("htmlDateString", (dateObj) => {
		// dateObj input: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-date-string
		return DateTime.fromJSDate(dateObj, { zone: "utc" }).toFormat("yyyy-LL-dd");
	});
```
You may be thinking that this all seems a convoluted, and I agree! If you know of a simpler way to accomplish this functionality, do feel free to let me know, but for now, I'm just happy that I finally have the ability to add image galleries to this website.

[^1]: Thanks to those who answered [my cry for help](https://lounge.town/@nathanu/113574428382435982)!
