🌓︎

From HTML to Eleventy

I have reworked this website for the 3rd time, this time using 11ty as the static site generator.

Unlike the previous two attempts 11ty has a good focus on extensibility which I'll cover towards the end, here's a brief tour of the websites history:

Cool things that Eleventy does differently

First of all, the barrier to entry is LOW, all you need is pnpm install @11ty/eleventy and right away it will start processing any file you place in this directory.

That's the second thing it does right, content is driven by structure, not by an arbitrary set of predefined places where templates and assets must be.

You also have the capability of building data driven content, for example the directory these blog posts are made in contains the following blog.11tydata.js file:

module.exports = {
    tags: ["blog"],
    layout: "layouts/post.liquid",
    permalink: function ({ title }) {
        return `/blog/${this.slugify(title)}/index.html`;
    },
    eleventyComputed: {
        eleventyExcludeFromCollections: (data) => {
            return data.utils.showDraft(data) ? data.eleventyExcludeFromCollections : true;
        },
        permalink: (data) => {
            return data.utils.showDraft(data) ? data.permalink : false;
        }
    }
};

This is automatically applied to each file and adds them to the "blog" collection by way of setting tags.

Visibility is controlled with the showDraft method, this isn't part of 11ty out of the box, it's just something you can implement according to your needs:

module.exports = {
    showDraft: (data) => {
        const todaysDate = new Date();
        const isDraft = 'draft' in data && data.draft !== false;
        const isFutureDate = data.page.date > todaysDate;
        return process.env.ELEVENTY_ENV === "dev" || (!isDraft && !isFutureDate);
    }
};

You'll also notice that data can cascade in an obvious way, lets say I manually set permalink in a md file in this folder, you can see that it will propagate upwards and override the default we have set in the data file.

All of this allows for great flexibility without having to know much, if anything, about the inner workings of 11ty itself.

Useful Plugins

Since 11ty is quite basic out of the box, there are some plugins that can be helpful if you're coming from a more featured platform.

Image

Uses sharp to process images, you can either switch it on globally to have optimized versions of all content generated, or you can use the library directly to get metadata and process them as you see fit.

Below is a gallery shortcode I implemented which creates a grid of thumbnails from a list of files. It outputs flex boxes with the image aspect ratio calculated ahead of time such that any number of different images are aligned horizontally without having to crop the content.

import sharp from 'sharp';
import Image from '@11ty/eleventy-img';

const GALLERY_IMAGE_WIDTH = 1024;

async function getGalleryImageData(src) {
    const metadata = await sharp(src).metadata();

    const options = {
        formats: ['jpg'],
        widths: [GALLERY_IMAGE_WIDTH],
        urlPath: "/images/thumbs/",
        outputDir: './_site/images/thumbs/'
    };

    const genMetadata = await Image(src, options);
    return {
        src,
        ar: parseFloat((metadata.width / metadata.height).toFixed(4)),
        thumbUrl: genMetadata.jpeg[0].url
    };
}

function parseGallerySources(content, extraSources = []) {
    const parsed = [];

    for (const line of content.split(/\r?\n/)) {
        let value = line.trim();
        if (!value) {
            continue;
        }

        value = value.replace(/^(?:[-*+]\s+|\d+\.\s+)/, "").trim();
        value = value.replace(/,$/, "").trim();

        if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
            value = value.slice(1, -1).trim();
        }

        if (value) {
            parsed.push(value);
        }
    }

    for (const source of extraSources) {
        if (typeof source === 'string' && source.trim().length > 0) {
            parsed.push(source.trim());
        }
    }

    return parsed;
}

export async function galleryShortcode(content, name, columns = 4, ...images) {
    const rowSize = Number(columns) || 4;
    const sources = parseGallerySources(content, images);
    const items = await Promise.all(sources.map((src) => getGalleryImageData(src)));

    let rows = '';
    for (let i = 0; i < items.length; i += rowSize) {
        const row = items.slice(i, i + rowSize);
        const rowHtml = row.map(({ src, ar, thumbUrl }) => {
            return `<a href="/${src}" data-ar="${ar}" style="flex-grow:${ar};aspect-ratio:${ar}">` +
                `<img src="${thumbUrl}" alt="" eleventy:ignore>` +
                `</a>`;
        }).join('');
        rows += `<div class="gallery-row">${rowHtml}</div>`;
    }

    return `<div><div class="gallery" id="gallery-${name}">${rows}</div></div>`;
}

Navigation

This plugin adds hierarchical navigation to 11ty, since collections are flat, there is no way to group articles by their hierarchy automatically. Using Navigation you set a key and parent and a map is built. Of course this is still a manual process so instead we can automate this with data.

Below is how the article directory is configured, key is derived from the title and only the parent is defined in the article itself.

module.exports = {
    tags: ["article"],
    layout: "layouts/article.liquid",
    permalink: function ({ title }) {
        return `/articles/${this.slugify(title)}/index.html`;
    },
    eleventyComputed: {
        eleventyNavigation: (data) => {
            if (!data.utils.showDraft(data)) return undefined;

            return {
                key: data.title,
                ...(data.parent ? { parent: data.parent } : {})
            };
        },

        eleventyExcludeFromCollections: (data) => {
            return data.utils.showDraft(data) ? data.eleventyExcludeFromCollections : true;
        },

        permalink: (data) => {
            return data.utils.showDraft(data) ? data.permalink : false;
        }
    }
};

Of course to use this map there are built in methods to filter and create lists, or you can iterate over the data manually and build any kind of presentation layer you wish.

---
title: Cool Stuff
parent: Articles
---

Various cool things...

If you find this site useful consider posting in the [Guestbook](/guestbook) (⁄ ⁄•⁄ω⁄•⁄ ⁄)

## Stuff
{{ collections.sortByTitle | eleventyNavigation: "Cool Stuff" | eleventyNavigationToHtml }}

## People

- [HamNB](https://hamnb.neocities.org/)

<p><img src="/images/wave.gif" style="margin-left:0;image-rendering: pixelated;" width="88"></p>

Conclusion

Time will tell if this is the right choice, it seems to be a very stable platform and is very easy to work with. Hopefully this is helpful if you're considering reworking your site soon.

Published on April 18, 2026