Responsive Images using Eleventy Shortcodes

Sia Karamalegos

Responsive images can be challenging to set up, from writing markup to generating the images. We can make this job much easier by using Cloudinary and Eleventy. Cloudinary can host and transform our images, making generation of many file formats and sizes a matter of adding a param to a URL. Eleventy is a hot JavaScript-based static site generator. It requires no client-side JavaScript, making it performant by default.

Originally, I did not set up Cloudinary on my Eleventy blog because I had a handful of images. I would create srcsets and formats using ImageMagick and cwebp. But then, I got excited about using structured data for SEO. The image generation job got a LOT more complicated with more sizes and cropping.

In this post, first I'll go over how to serve responsive, performant images. Then, I'll show you how I implemented Cloudinary image hosting in Eleventy using Eleventy shortcodes.

What's in an <img>?

Let's take a look at a "fully-loaded" image tag in HTML:

1<img src="pug_life.jpg"
2 srcset="pug_life_600.jpg 600w, pug_life_300.jpg 300w,"
3 sizes="(min-width: 760px) 600px, 300px"
4 alt="Pug wearing a striped shirt"
5 width="600"
6 height="400"
7 loading="lazy"
8 >

Why did I include all those attributes? Let's take a look at each...

  • src - the image to display (required!)
  • srcset - for modern browsers, a set of candidate images and their widths in pixels
  • sizes - for modern browsers, how wide the image will be displayed at various screen widths
  • alt - description of the image
  • width - the image width
  • height - the image height
  • loading - whether to lazy-load images and iframes (check for support with caniuse)

srcset and sizes

srcset and sizes give modern browsers a set of images and instructions for how wide they will be displayed. The browser will make the best decision on which image to load based on the user's screen width and device pixel ratio (DPR). For example, those nice Retina screens (DPR of 2) need images twice as wide as the slot we're putting them in if we want them to look good. Stated another way, if your CSS says to display an image at 100px wide, we need to supply an image that is 200px wide.

The sizes attribute can be tricky to write by hand. My favorite way (a.k.a, the lazy way) is to first give the image a srcset, then run the page through RespImageLint. RespImageLint is a nifty bookmarklet that analyzes the images on a webpage. It will tell you how far off your images are in their size and will also give suggestions for the sizes attribute:

Layout Shift

To prevent layout shift once the image loads, we need to provide the browser with an aspect ratio. Currently, the way to do that is to set a height and width on the image in HTML. Use the original image's dimensions since the actual size doesn't matter. The aspect ratio is what is important. Your CSS will control the actual height and width.

To prevent weird stretching, set an auto height in your CSS:

1img {
2 max-width: 100%;
3 height: auto;
4}

Jen Simmons recorded a great short video on this trick.

Lazy loading

We now have partial support for lazy loading images and iframes! If you set the loading attribute to lazy, the browser will use the IntersectionObserver to detect if a user scrolls near the image or iframe and only load it at that time.

At the time of writing, 78% of my blog's visitors use browsers that support native lazy loading. Thus, I'm implementing it now. Import your Google Analytics data into caniuse to see how many of your visitors have support for any given web feature.

Note that you should not lazy-load images that are in the viewport on initial load ("above the fold"). This can lower your performance scores.

Eleventy Shortcodes and Filters

What are Eleventy shortcodes? Shortcodes are like to filters in that they allow us to reuse code. Nunjucks, Liquid, and Handlebars templates all support both shortcodes and filters. For simplicity, the rest of this post will use Nunjucks.

1<!-- FILTER using Nunjucks -->
2<!-- The filter, or function, is makeUppercase, and the first and only parameter is name. -->
3<h1>{{ name | makeUppercase }}</h1>
4
5<!-- SHORTCODE using Nunjucks -->
6<!-- The shortcode is user, and the parameters are firstName and lastName. -->
7{% user firstName, lastName %}

For this use case, we could use either. I chose shortcodes since most of the other solutions use them and I wanted to try them out for the first time.

The code

Now that you know how I make images responsive, I can explain the rationale behind my solution.

The existing solutions used shortcodes that provided the full image tag based on the filename, alt, and a few other attributes. I wanted the ability to also provide all the attributes previously mentioned (loading, sizes, etc.) plus others like class.

The shortcode became unwieldy with this many parameters. Then I realized that the HTML itself was only marginally longer. Why not use HTML? The onerous part of writing the markup is setting the image urls and generating the srcsets.

Why not use HTML?

Hence, I created shortcodes that do only that - generate the src and srcset. Everything else can be set as needed in the HTML:

1<img src="{% src 'possum_film_director.jpg' %}"
2 srcset="{% srcset 'possum_film_director.jpg' %}"
3 sizes="{% defaultSizes %}"
4 alt="Possum directing a movie"
5 width="2953"
6 height="2178"
7 loading="lazy"
8 class="super-great-style-class"
9 >

I don't need a <picture> tag. Cloudinary can serve the best image format based on the user's browser through the f_auto transformation.

Shortcodes

I gave the shortcodes smart default widths based on the styles for my site, but I allow an optional parameter to set them when I invoke the shortcode. The first step is to set our constants:

1// _11ty/shortcodes.js
2
3// Set constants for the Cloudinary URL and fallback widths for images when not supplied by the shorcode params
4const CLOUDNAME = "[your Cloudinary cloud name]"
5const FOLDER = "[optional asset folder in Cloudinary]"
6const BASE_URL = `https://res.cloudinary.com/${CLOUDNAME}/image/upload/`;
7const FALLBACK_WIDTHS = [ 300, 600, 680, 1360 ];
8const FALLBACK_WIDTH = 680;
9
10// ...

Then, we can define the shortcodes to create a src and reuse that function to create a srcset. These use the given widths or our fallback widths from the constants previously set:

1// _11ty/shortcodes.js
2// ...
3
4// Generate srcset attribute using the fallback widths or a supplied array of widths
5function getSrcset(file, widths) {
6 const widthSet = widths ? widths : FALLBACK_WIDTHS
7 return widthSet.map(width => {
8 return `${getSrc(file, width)} ${width}w`;
9 }).join(", ")
10}
11
12// Generate the src attribute using the fallback width, or a width supplied by the shortcode params
13function getSrc(file, width) {
14 return `${BASE_URL}q_auto,f_auto,w_${width ? width : FALLBACK_WIDTH}/${FOLDER}${file}`
15}
16
17// ...

The final step in our shortcodes file is to export the two shortcodes to access them in our Eleventy config:

1// _11ty/shortcodes.js
2// ...
3
4// Export the two shortcodes to be able to access them in our Eleventy config
5module.exports = {
6 srcset: (file, widths) => getSrcset(file, widths),
7 src: (file, width) => getSrc(file, width),
8}

Now we can add the shortcodes to our Eleventy config:

1// .eleventy.js
2const { srcset, src } = require("./_11ty/shortcodes");
3
4eleventyConfig.addShortcode('src', src);
5eleventyConfig.addShortcode('srcset', srcset);

Voilà!

Conclusion

Eleventy shortcodes help us make the troublesome process of generating source sets for our images easier. We can use the flexibility of HTML to customize all the other behavior we want (e.g., lazy loading, classes, etc.).

Check out the full demo on CodeSandbox:

How do you use Eleventy with Cloudinary? I haven't turned this into a plugin yet. Should I? Ping me on Twitter with your thoughts!

More resources:

Cover image from Wikimedia Commons

Sia Karamalegos

Web Developer & Performance Engineer

Sia Karamalegos is a developer, international conference speaker, and writer. She is a Google Developer Expert in Web Technologies, a Cloudinary Media Developer Expert, and a Women Techmakers ambassador. She co-organizes GDG New Orleans and its marquee event, DevFest New Orleans. She is the founder and lead developer for Clio + Calliope Web Development and was recognized in the Silicon Bayou 100, the 100 most influential and active people in tech and entrepreneurship in Louisiana.