diff --git a/src/posts/2024/check-if-images-are-available-before-optimizing-in-eleventy.md b/src/posts/2024/check-if-images-are-available-before-optimizing-in-eleventy.md new file mode 100644 index 00000000..3da571eb --- /dev/null +++ b/src/posts/2024/check-if-images-are-available-before-optimizing-in-eleventy.md @@ -0,0 +1,121 @@ +--- +date: '2024-02-05' +title: 'Check if images are available before optimizing in Eleventy' +description: 'How to check if a remote image is available before attempting to optimize it using @11ty/eleventy-img.' +tags: ['development', 'javascript', 'Eleventy'] +--- +I updated my image shortcode recently to leverage [Lene Saile](https://www.lenesaile.com)'s shortcode in the ever-useful [eleventy-excellent](https://github.com/madrilene/eleventy-excellent) project. As part of making this change, I also wanted to address build issues I had been facing when attempting to fetch and display avatars in webmentions (in the interest of improving post page performance). + +When fetching webmention avatars I would see occasional issues where one would be unavailable and cause the build to fail when it encountered a response other than `200`. To work around this, I created a simple message to check the headers for remote images before attempting to optimize them, which looks like this: + +```javascript +const imageExists = async () => { + try { + return await fetch(src, { method: 'HEAD' }).then((res) => res.ok); + } catch { + return false; + } +}; +``` + +This returns a simple `boolean` that can be checked before calling `Image` from `@11ty/eleventy-img`. Additionally, we don't want to fetch locally stored images and, as such, we check whether an image `isLocal` before calling `imageExists`: + +```javascript +const isLocal = src.includes('src/assets'); +``` + +The full code is below, but the actual image is returned out of this fun, fun nested ternary: + +```javascript +return isLocal ? await generateImage() : await imageExists().then(async (exists) => (exists ? await generateImage() : await generatePlaceholder())); +``` + +If an image is local, `generateImage()`, otherwise check if the `imageExists()` then either `generateImage()` or `generatePlaceholder()`. The placeholder, in my case, is a div with a black background an an `svg` captured and passed into the `image` shortcode for cases where I expect some failures (remote images outside of the CDN I use, mostly). + +```javascript +import Image from '@11ty/eleventy-img'; +import path from 'path'; +import htmlmin from 'html-minifier-terser'; + +const stringifyAttributes = (attributeMap) => + Object.entries(attributeMap) + .map(([attribute, value]) => (value === undefined ? '' : `${attribute}="${value}"`)) + .join(' '); + +export const img = async ( + src, + alt = '', + className, + loading = 'lazy', + shape = '', + icon, + maxWidth = 1248, + sizes = '90vw', + formats = ['avif', 'webp', 'jpeg'] +) => { + const isLocal = src.includes('src/assets'); + const imageExists = async () => { + try { + return await fetch(src, { method: 'HEAD' }).then((res) => res.ok); + } catch { + return false; + } + }; + + const generateImage = async () => { + const widths = [320, 570, 880, 1024, 1248]; + const metadata = await Image(src, { + widths: widths.filter((width) => width <= maxWidth), + formats: [...formats], + outputDir: './_site/assets/img/cache/', + urlPath: '/assets/img/cache/', + filenameFormat: (id, src, width, format) => { + const { name } = path.parse(src); + return `${name}-${width}w.${format}`; + }, + }); + const lowsrc = metadata.jpeg[metadata.jpeg.length - 1]; + const imageSources = Object.values(metadata) + .map( + (imageFormat) => + `` + ) + .join('\n'); + const imgageAttributes = stringifyAttributes({ + src: lowsrc.url, + width: lowsrc.width, + height: lowsrc.height, + alt, + class: className, + loading, + decoding: 'async', + }); + const imageElement = `${imageSources}`; + + return htmlmin.minify(imageElement, { collapseWhitespace: true }); + }; + + const generatePlaceholder = async () => { + return htmlmin.minify( + `
${icon}
`, + { collapseWhitespace: true } + ); + }; + + return isLocal ? await generateImage() : await imageExists().then(async (exists) => (exists ? await generateImage() : await generatePlaceholder())); +}; +``` + +Here's an example use case from my `interaction.liquid` partial used in my webmentions implementation: + +{% raw %} +```liquid +{% capture authorAlt %}{{ mention.author.name | escape }}{% endcapture %} +{% capture fallbackIcon %}{% tablericon "user" authorAlt %}{% endcapture %} +{% image mention.author.photo, authorAlt, 'avatar__image', 'lazy', 'square', fallbackIcon %} +``` +{% endraw %} + +With all that in place I can now build my site, check if an image exists and spit out a servicable placeholder without failing the build. \ No newline at end of file