--- title: Responsive, Self-hosted Images for Your Eleventy Blog image: src: https://img.sebin-nyshkim.net/i/f33879d8-3f98-4c8e-a1b3-08edf6f174ac alt: Close-up of SVG code on a computer screen credit: Photo by Florian Olivo on Unsplash width: 1200 height: 630 type: 'image/png' tags: ["self-hosting", "docker", "eleventy"] --- While you can certainly host your image files with the Git repo your Eleventy site is checked into, or add them manually after building it, neither option is ideal if you want responsive images in multiple formats to save precious bandwidth. GitHub imposes [limits](https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-large-files-on-github) on the size of files you can commit, and when your repo reaches a certain size, GitHub will ask you nicely to reduce its size (or else…) There's Git LFS, but even then, having to check out several hundreds of megabytes of assets (or more) when you're cloning your repo is equally unwieldy and the costs for storage and bandwidth can quickly add up as the repo grows. Fortunately, Eleventy provides the `@11ty/eleventy-img` plugin, which is very capable when it comes to transformations and responsive images. It works not only with local files, but also with remote resources and includes them in the output during a build. You could certainly host your images on an external site like Imgur and have `@11ty/eleventy-img` pull them in for processing to avoid having to put them in your Git repo. However, external hosting services may have their own restrictions on what you can upload to them (file size, resolution, formats). And with every internet company trying to jump on the "AI" bandwagon, no matter how small, no matter how unnecessary, I'd be very wary of trusting someone else with my blog's images. So self-hosting is pretty much the only other option. I found a [repo on GitHub](https://github.com/awesome-selfhosted/awesome-selfhosted#photo-and-video-galleries) that lists a number of self-hosting services, among them a couple for image hosting. One of them, which I would like to cover in this post, is called PicoShare. It can be used to host not only images, but also videos, zip archives and much more. It just hosts the files, it doesn't process them, so it's perfect for running on that Raspberry Pi you've got lying around in a drawer somewhere. ## Setting up PicoShare > [!info] Note > I'm assuming you already know how to use Docker. [PicoShare](https://github.com/mtlynch/picoshare) is single-user only, but for the purpose of having a super simple file hosting solution with an equally simple web UI, this is entirely sufficient. If you need to give other people access to it, you can do so with its "Guest Links" feature and they can upload something to it. Here's my `docker-compose.yml` I use to run PicoShare: ```yaml version: "3.2" services: picoshare: image: mtlynch/picoshare container_name: picoshare environment: PORT: 4001 PS_BEHIND_PROXY: true PS_SHARED_SECRET: SuperSecretPassNobodyWillEverKnow ports: - 4001:4001 volumes: - picoshare:/data volumes: picoshare: ``` The PicoShare `README.md` doesn't use volumes in its compose file example, but it was the only way for me to work around weird permission issues with bind mounts on Podman. I'm also running it behind a reverse proxy, so I have the `PS_BEHIND_PROXY` environment variable set. This is so I can have a memorable host name as well as TLS termination, which spares me having to get a Let's Encrypt certificate for every individual service I host. So after a quick `docker compose up -d` PicoShare should be up and running in no time and visiting `http://localhost:4001` should give your its greeting page. ![PicoShare greeting page](https://img.sebin-nyshkim.net/i/6df54a9e-e42f-4c6d-bd2d-105ff4e1cc16) From there you can log in with the passphrase you set in the `docker-compose.yml` and start uploading some files. By default, PicoShare sets an expiration period of 30 days on every upload. If you plan on using it for blog post images, that's probably not what you want, so you may want to disable that in the settings. Once you upload a file, PicoShare shows you a full link and a short link. You can use either to link to an image for linking an image on a website. ## Responsive Images in Eleventy In order to have responsive images in the blog at all, you have to customize the config and configure the image transform plugin. Install the `@11ty/eleventy-img` plugin via NPM and add it with the `addPlugin()` helper function with some options: ```js import { eleventyImageTransformPlugin } from '@11ty/eleventy-img'; export default async function (eleventyConfig) { eleventyConfig.addPlugin(eleventyImageTransformPlugin, { extensions: "html", formats: ["webp", "jpeg"], defaultAttributes: { loading: "lazy", decoding: "async", }, }); } ``` The Eleventy image transform plugin is highly configurable. Below are the most common options, followed by more advanced options. +++ Basic options `extensions` – Type: `string` – Detault: `"html"` : Comma separated list of which extensions to scan for `` tags to transform into responsive `` tags. `widths` – Type: `Array` – Default: `[null]` : A list of resolutions in which the images should be created. Their aspect ratio will be kept automatically. Including `auto` or `null` in the list will also include a converted version of the image in its original resolution. **Important:** When including more than one resolution the `sizes` attribute becomes obligatory. See `defaultAttributes` below. `formats` – Type: `Array` – Default: `['webp', 'jpeg']` : A list of formats images should be converted into. Accepted formats are: - `auto`/`null` (same as input) - `svg`/`svg+xml` (only applies when the input is SVG) - `jpeg`/`jpg` - `png` - `webp` - `avif` `concurrency` – Type: `number`– Default: `10` : How many concurrent threads should be used for image transformation. `urlPath` – Type: `string` – Default: `"/img/"` : A path that gets prefixed to the `src` attribute of `` tags. `outputDir` – Type: `string` – Default: `"img/"` : The output directory for transformed images, relative to the Eleventy output directory. `svgShortCircuit` – Type: `boolean | "size"` – Default: `false` : Whether or not to skip raster formats for SVG. Only applies when `svg` is included in the `formats` array. - `false` (default): SVG images will be converted into raster images. - `true`: SVG images will not be converted into raster images, even if `svg` is part of the `formats` array of image types to be converted into. Useful when you can't say for sure if the input will be raster or vector based and you want to always keep SVGs as vector based images. - `size`: SVG images will only be converted into raster images, if the resulting output file is smaller than the original SVG file. `svgAllowUpscale` – Type: `boolean` – Default: `true` : Whether or not to upscale SVG images before they are converted into raster images. `svgCompressionSize` – Type: `string` – Default: `""` (empty string) : Set to `br` to report an SVG's size as it would be after being compressed with [Brotli](https://www.brotli.org/). `defaultAttributes` – Type: `object` – Default: `undefined` : HTML attributes to add to every processed image. Can be any valid HTML attribute for [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attributes) or [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source#attributes). +++ For the most common use-cases, the default options get the job done pretty well. If you want to fine-tune the inner workings of the Eleventy image transform plugin even further, refer to the advanced options below: +++ Advanced options `sharpOptions` – Type: `sharp.SharpOptions` – Default: See [Sharp docs](https://sharp.pixelplumbing.com/api-constructor) : An options object to pass to the Sharp constructor. `sharpWebpOptions` – Type: `sharp.WebpOptions` – Default: See [Sharp docs](https://sharp.pixelplumbing.com/api-output#webp) : Options for how WebP images should be processed by Sharp. `sharpPngOptions` – Type: `sharp.PngOptions` – Default: See [Sharp docs](https://sharp.pixelplumbing.com/api-output#png) : Options for how PNG images should be processed by Sharp. `sharpJpegOptions` – Type: `sharp.JpegOptions` – Default: See [Sharp docs](https://sharp.pixelplumbing.com/api-output#jpeg) : Options for how JPEG images should be processed by Sharp. `sharpAvifOptions` – Type: `sharp.AvifOptions` – Default: See [Sharp docs](https://sharp.pixelplumbing.com/api-output#avif) : Options for how AVIF images should be processed by Sharp. `cacheOptions` – Type: `CacheOptions` – Default: See below : Controls how external sources should be cached. `type` – Type: `string` – Default: `'buffer'` : The expected content type of the response when fetching the remote resource. Accepts `buffer` (binary data), `json` or `text`. `directory` – Type: `string` – Default: `".cache"` : Directory in which to store cached resources. Relative to the project directory. `fetchOptions` – Type: `RequestInit` – Default: See [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit) : Options for configuring the behavior of the JavaScript Fetch API used to fetch resources. `concurrency` – Type: `number` – Default: `10` : How many concurrent fetch operations are allowed. `duration` – Type: `string` – Default: `'1d'` : Time that needs to pass before fetching an already fetched resource again. `removeUrlQueryParams` – Type: `boolean` – Default: `undefined` : Whether or not to remove the query parameters from URLs cached by Eleventy Image. `dryRun` – Type: `boolean` – Default: `undefined` : Only simulate doing something. `verbose` – Type: `boolean` – Default: `undefined` : Control verbosity of the cache. `hashLength` – Type: `number` – Default: `30` : Truncates hash to this length. `filenameFormat` – Type: `function(options): string | null | undefined` : A function to customize the file names of transformed images in the final output. Returns either `string`, `null` or `undefined` `options` – Type: `object` : `id` – Type: `string` : Hash of the original image `src` – Type: `string` : Original image path `width` – Type: `number` : Current width in px `format` – Type: `string` : Current file format `options` – Type: `ImageOptions` : An Eleventy Image options object (yes, the one you're reading right now) `urlFormat` – Type: `function(format, options): string` : A function to control how the URLs will look like when processed by Eleventy Image. When setting this function, its parameters are obligatory. Useful when you already have your own image processing service and saving the images with the site isn't necessary or desired. Returns a string (the final URL). `format` – Type: `object` : `hash` – Type: `string` : hash of the remote image. Not included for `statsOnly` images. `src` – Type: `string` : source file name of the remote image. `width` – Type: `number` : output width the remote image should have. `format` – Type: `string` : image format the remote image should have. `options` – Type: `ImageOptions` : An Eleventy Image options object (yes, the one you're reading right now) `statsOnly` – Type: `boolean` – Default: `undefined` : Whether or not to process files. If `true` only returned stats of images, doesn't read or write any files. Useful in combination with `urlFormat()`. `useCache` – Type: `boolean` – Default: `true` : Whether or not to use in-memory cache. `dryRun` – Type: `boolean` – Default: `undefined` : Whether or not to write anything to disk. A buffer instance will still be returned. `hashLength` – Type: `number` – Default: `10` : The maximum length of file name hashes. `useCacheValidityInHash` – Type: `true` – Default: `true` : Advanced `fixOrientation` – Type: `boolean` – Default: `false` : Whether or not to rotate images to ensure correct orientation. `minimumThreshold` – Type: `number` – Default: `1.25` : Ensures original size is included if smaller than largest specified width by threshold amount. +++ After you've set up the plugin to your needs, whenever you're using an `` in your templates (or the equivalent syntax in Markdown: `![alt text](url)`), the plugin will transform the referenced image according to your options and put a corresponding `` tag in the resulting output. > [!tip] Tip > If you want a side by side comparison to decide how to fine-tune the image compressors, check out [Squoosh](https://squoosh.app/). Some recommendations for a config with some options set: ```js import { eleventyImageTransformPlugin } from '@11ty/eleventy-img'; export default async function (eleventyConfig) { eleventyConfig.addPlugin(eleventyImageTransformPlugin, { extensions: 'html', formats: ['avif', 'webp', 'auto'], // commonly supported image formats widths: [640, 1280, 1920, 3840, 'auto'], // image sizes, from mobile up to 4K sharpJpegOptions: { mozjpeg: true, // more efficient compression for JPEGs optimiseScans: true, // progressive loading quality: 95 // higher quality }, sharpPngOptions: { compressionLevel: 9 // max compression for PNGs }, urlPath: '/img/', // prefix for generated outputDir: './public/img/', // put all transformed images in the same place defaultAttributes: { loading: 'lazy', // only load images once they come into view decoding: 'async', // render images after DOM content has rendered sizes: '100vw' // when more than 1 value in widths, set this } }); } ``` > [!attention] Attention > When specifying multiple values for `widths` you will also have to specify a `sizes` option in `defaultAttributes`. From [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img): > > If the `srcset` attribute uses width descriptors, the `sizes` attribute must also be present, or the `srcset` itself will be ignored. > > The Eleventy Image docs don't explicitly mention this[^docs]. > [!info] Info > The order of `formats` corresponds to the order of the `` elements in the final output. The browser picks the first format in the list it supports. If you want it to use AVIF before WebP, make sure the list starts with `avif`. Now you can just point to any image, local or remote, and the image transform plugin will download it, convert and transform it and include all sizes and formats in the output. ### Responsive CSS Background Images with `image-set()` The docs advertise the image transform plugin to also handle CSS `background-image` but I don't find it working. Neither by adding `css` to the `extensions` option, nor assigning `background-image` in the CSS for my site or directly in my `base.njk` template in a `