Skip to main content
Ungathered Thoughts

Eleventy 🤝 Immich

Lately I've been exploring self-hosting personal data, and photo storage, management, discovery and sharing is one service I'm keen to move from a megacorp-hosted option to self-hosted. Several weeks back I gave PhotoPrism a trial, and I'm now checking out Immich. I'm using Eleventy for my personal website and one thing I wanted to test was the ability to integrate the two.

June 2024 update ✨ ⭐

The plugin referred to in this post is now available via
npm install @xurizaemon/eleventy-immich or
https://github.com/xurizaemon/eleventy-immich

Immich permits album sharing, but for public display I prefer to selectively pull some albums or images into my static build pipeline - I'd rather put my faith in a static publishing output than take on the maintenance and securing of a complex service - life is too precious and short. I want to be able to reference photos from my image collection into my writing.

This last weekend I've got a prototype up and running of embedding Immich assets in Eleventy, and here's an update! All images in this post are film photos taken Summer 2023/2024 using a Lomo Fisheye and Orca black and white 110 film.

Shortcodes for immich_image and immich_album

Here's an image I'm embedding which was sourced from Immich when building this site. A custom shortcode has been used to generate an image tag using the UUID of the image in Immich.

{% immich_image '3d0bd5db-a237-4a5e-8994-6ff91fa62878' %}
Black and white fish-eye lens film photograph, people sitting under a sun shade at the beach.

Here's an album via the immich_album shortcode. I haven't attempted to format this in a meaningful way, only to emit some details about the album and then a series of photos. This part will call for templates that can be overridden in the project.

{% immich_album 'ef43ecca-f88d-4f5a-b590-533d0611e045' %}

Lomo Baby

A few selected photos on the Baby Lomo.

a selfie at the Dunedin Farmers MarketMum and me in her gardenHunter and Saira

Next steps

Implementation

Here's what I currently have.

Configuration

Right now, the plugin supports only a single Immich instance and user account, via environment variables. Your build environment must be able to make HTTP requests to your Immich API URL. You'll also need an API key from your Immich instance - see /user-settings there to create a key for your account. Set the required variables in your build environment:

IMMICH_BASE_URL=https://immich.example.org
IMMICH_API_KEY=ABCD1abcd1ABCD1abcd1ABCD1abcd1ABCD1abcd1AB

I'm running this by adding ./immich.js to my codebase, next to the Eleventy configuration file. I've copied the code to https://github.com/xurizaemon/eleventy-immich as well - but I haven't even switched my own use to that yet.

./immich.js contents:

/**
* A plugin to provide Immich shortcodes for Eleventy.
*/


const EleventyFetch = require("@11ty/eleventy-fetch");
const Image = require("@11ty/eleventy-img");

module.exports = function immich(eleventyConfig) {
let required_vars = ['IMMICH_BASE_URL', 'IMMICH_API_KEY'];
for (name of required_vars) {
if (!process.env[name]) {
console.error(`Immich plugin requires setting the following env vars: ${required_vars.join(', ')}`);
// @TODO Can I bail out here somehow?
}
}

let immichUrl = `${process.env.IMMICH_BASE_URL}`;
let apiKey = `${process.env.IMMICH_API_KEY}`;
let defaultFetchOptions = {
headers: {
'x-api-key': apiKey,
}
};

/**
* Fetches an album and renders each asset.
*
* @param uuid
* @returns {Promise<string>}
*/

async function immichAlbumShortcode(uuid) {
let albumUrl = `${immichUrl}/api/album/${uuid}`;

let albumData = await EleventyFetch(albumUrl, {
duration: "10m",
type: "json",
fetchOptions: {
...defaultFetchOptions,
...{ accept: 'application/json' }
}
});

let html = `<div class="immich-album"><h2>${albumData.albumName}</h2>`;
if (!!albumData.description) {
html += `<p>${albumData.description}</p>`;
}
if (albumData.assets.length) {
html += '<div class="immich-album-assets">';
for (asset of albumData.assets) {
html += await immichImageShortcode(asset.id);
}
html += '</div>';
}
html += '</div>';
return html;
}

/**
* Fetches image data and file from a given UUID and generates HTML for the image element with specified attributes.
*
* @param {string} uuid - The UUID of the image asset.
* @returns {Promise<string>} - A promise that resolves to the generated HTML for the image element.
*/

async function immichImageShortcode(uuid) {
let assetDataUrl = `${immichUrl}/api/asset/assetById/${uuid}`;
let assetFileUrl = `${immichUrl}/api/asset/file/${uuid}`;
let fetchOptions = defaultFetchOptions;

fetchOptions.headers.accept = 'application/json';
let assetData = await EleventyFetch(assetDataUrl, {
duration: "10m",
type: "json",
fetchOptions: fetchOptions
});

fetchOptions.headers.accept = 'application/octet-stream';
let assetFile = await EleventyFetch(assetFileUrl, {
duration: "1w",
type: "buffer",
fetchOptions: fetchOptions
});

let alt = assetData.exifInfo.description ?? 'No image description available';

let metadata = await Image(assetFile, {
widths: [300, 600],
formats: ["jpeg"],
outputDir: 'public/media/img/',
urlPath: '/media/img/'
});

let attributes = {
alt: alt,
sizes: [],
loading: "lazy",
decoding: "async",
};

return Promise.resolve(Image.generateHTML(metadata, attributes));
}

/**
* Retrieves a collection of Immich albums.
*
* @returns {Promise<Array>} A promise that resolves to an array of albums.
* Each album is represented as an object in the array.
* If an error occurs, an empty array is returned.
*/

async function immichAlbumsCollection() {
try {
let albums = await EleventyFetch(`${immichUrl}/api/album`, {
duration: "1d",
type: "json",
fetchOptions: {
...defaultFetchOptions,
...{ accept: 'application/json' }
}
});

for (album of albums) {
collection.push(album);
}
return collection;
}
catch(e) {
console.log( "Failed getting Immich albums" );
console.log(e, 'exception');
}
}

// eleventyConfig.addCollection("immich_albums", immichAlbumsCollection);
eleventyConfig.addShortcode("immich_album", immichAlbumShortcode);
eleventyConfig.addShortcode("immich_image", immichImageShortcode);
};

We add the plugin to Eleventy by modifying the eleventy configuration file, eg this .eleventy.js addition:

module.exports = (eleventyConfig) => {
// ...

eleventyConfig.addPlugin(require('./immich'));

// ...
};

PS. Hopefully you saw the update at the top - this plugin is now available as an NPM module, see https://github.com/xurizaemon/eleventy-immich or npm install @xurizaemon/eleventy-immich - it's still WIP and contributions are welcome!