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' %}
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.
Next steps
- I implemented a collection so I could iterate through Immich photo albums - that was a start point, but it's removed now as I didn't want to leave example code in place that would reveal updates to the photo albums over time. I will come back to that to implement filtering (ie "show me albums that are marked public or shared").
- It would be interesting to provide front matter such as
immich_header_image
to pull an image in for the page's feature image? - I am keen to experiment with turning the output into
.webc
components instead of shortcodes. - This can become an installable Eleventy plugin ... but it's not ready yet! Watch https://github.com/xurizaemon/eleventy-immich for updates.
- Performance wants review. While I'm using Eleventy Fetch and I think it's caching, the build time has spiked from ~30s to ~2min, and we only have 24 images in this post. The
.cache/
directory seems to be getting populated and staying about the same size. - Direct embeds are demonstrated above, calling an image or album into a post by some reference. I'd like to explore configuring a set of albums and having Eleventy build out a section of my website with those pages based on the data retrieved, and with URLs for each album or image media.
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!