# tlohde

exhibitionism

13:16 19/09/2025
1060 words
contents

I made a gallery. And I think I've finished tinkering with it.

It's static. There ain't no js here.

motivation

To have somewhere to put my maps. And maybe, maybe, this will motivate me to make a few more. At the moment it is mostly populated with ones made during the last two 30 Day Map Challenges.

assistance

I leaned heavily on:

Along the way to my solution, I tried and borrowed various bits and pieces from

how

the data

A maps.json file in src/_data that looks a bit like:

[
  {
    "title": "the title of my map",
    "src": "where/it/is/saved.png",
    "alt": "appropriate alt text - this is a must",
    "caption": "A nice caption",
    "description": "this is for open graph",
    "tools": "what i used to make the map (QGIS, python, pen & paper)",
    "data": "data sources",
    "font": "SuperCursive, LameSerifs"
  }
]

Whilst editing text in a json is a bit gross and annoying at times, this seemed like a reasonably rational choice.

the shortcode

All of my shortcodes live in _config/shortcodes.js[1]. There the following was added:

/* for gallery images */
const PREVIEW_HEIGHT = 300;

async function img(image, css, loading='lazy') {
    if (image.alt == undefined) {
        throw new Error(`missing alt`);
    }
    let metadata = await Image(image.src, {
        formats: ["webp"],
        widths: [200, 300, 480, 640, 1024, 2048],
        outputDir: './src/images',
        urlPath: "/images/",

        filenameFormat: function (id, src, width, format, options) {
            const ext = path.extname(src);
            const name = path.basename(src, ext);
            return `${name}-${width}w.${format}`;
        },
        htmlOptions: {
            imgAttributes: {
                loading: "lazy",
                decoding: "async"
            },
            pictureAttributes: {class: "something"}
        },
    });

    let preview = metadata.webp.find(i => i.height >= PREVIEW_HEIGHT) || metadata.webp[0];
    let full = metadata.webp[metadata.webp.length - 1];

    const srcset = Object.values(metadata).map((imageFormat) => {
        return imageFormat.map(entry => entry.srcset).join(", ");
    })

    if (css == 'preview') {
        let slug = slugify(image.title);
        return `
            <a href="/mapping/gallery/${slug}">
            <img
            class="${css}"
            src="${preview.url}"
            alt="${image.alt}"
            >
            </a>
            `.replace(/(\r\n|\n|\r)/gm, "")
    } else {
        return `
        <img
        src="${full.url}"
        alt="${image.alt}"
        srcset="${srcset}"
        fetchpriority="high"
        loading="eager"
        sizes="auto"
        />
        `
    }
}

// export
export const shortcodes = (eleventyConfig) => {
    eleventyConfig.addShortcode("img", img);
};

Is this perfect? No.

Could this be improved? Yes.

the loop

A new page[2] was created. And there the following bit of html / nunjucks:

<div class="outerGallery">
  {%- for map in maps -%}
    <div class='gallery'>
      {%- img map, 'imgPreview' -%}
    </div>
  {%- endfor -%}
</div>
</div>

This loops through all the items in maps.json, and applies the img shortcode to each one. Bingo.

the syling

A bit more css than usual. This is inlined with {% include "./gallery/galleryStyles.njk" %}[3].

The whole page has some hackneyed contours in the background[4]. The background .svg changes between dark and light mode like so

[data-theme='dark'] body {
  background-image: url('/assets/contoursDark.svg');
}

[data-theme='light'] body {
  background-image: url('/assets/contours.svg');
}

The outerGallery spans 90vw, and has a fixed height of 340px, and it can be scrolled horizontally. Each inner gallery item, and the styling on the img itself, means that all images are scaled to same height. On hover there's a transform: scale(1.05) rotate(3deg) and the box-shadow gets a bit bigger.

The images here are the preview ones generated by the img shortcode - so they're nice and small. And, with the loading=lazy attribute, the images only load when scrolled to. So the whole thing is pretty snappy[5].

Clicking on a map from the gallery takes you to its individual page.

the pages

Three cheers for 11ty pagination

In src/mapping/gallery.njk is the front matter:

---
layout: "layouts/mapGallery.njk"

pagination:
  data: maps
  size: 1
  alias: map

permalink: "/mapping/gallery/{{ map.title | slugify }}/"
---

which makes a page for each map in the maps.json, and gives it a sensible url.

layouts/mapGallery.njk isn't too different from my base layout, but here I make use of the metagen plugin to populate some page metadata:

  {% metagen
  title=map.title,
  desc=map.description,
  url=url + map.title | slug + "/",
  img=map.src,
  alt=map.alt
  %}

A higher resolution version of the map is shown, and it's styled to try and take up 95svh, provided that wouldn't make it wider than the viewport. fetchpriority="high" and loading="eager" means[6], the image gets loaded asap.

The image comes with alt text, and a nice caption. A table below shows the tools used to make the map, along with any data sources and fonts used.

Finally, at the bottom there's the navigation, which grabs the name of the next/previous map from the pagination object...

<nav style='display:flex; flex-flow: column nowrap; justify-content:center; align-items: center; margin:2rem auto;'>
  {% if pagination.href.previous %}
    <a class="link" href="{{ pagination.href.previous }}">{{ pagination.page.previous.title }}</a>
  {% endif %}
    <span style="margin: 0.5lh 0;">back to the <a class="link" href="/mapping">map room</a></span>
  {% if pagination.href.next %}
    {% if not pagination.href.previous %}
      <a class="link" href="{{ pagination.href.next }}">{{ pagination.page.next.title }}</a>
    {% else %}
      <a class="link" href="{{ pagination.href.next }}">{{ pagination.page.next.title }}</a>
    {% endif %}
  {% endif %}
</nav>

still to do

make some more maps.


happy browsing

footnotes


  1. alongside _config/index.js which has the line export { shortcodes } from "./shortcodes.js", and they're added to .eleventy.js with eleventyConfig.addPlugin(shortcodes) ↩︎

  2. src/mapping/index.md ↩︎

  3. feel free to right-click insepct on the mapping page - if you must ↩︎

  4. contours of my favourite place, courtesy of Copernicus Global DEM (ESA) ↩︎

  5. full marks on lighthouse, thank you very much ↩︎

  6. i think ↩︎


have thoughts? want to share? email me, or find me on mastodon where you can reply to this post