Skip to content

Responsive Images in Eleventy Markdown

I added image support to my Eleventy website. I was looking for a solution that would automatically take a lot of the grunt work out of preparing images for the web. When it comes to images, there's actually a lot to consider:

  • You should serve images with the correct dimensions so they don't get distorted
  • You should use srcset & sizes attributes to load different sized images based your user's viewport
  • You should serve images in various formats (jpeg, avif, webp) so that modern browsers can load the page faster
  • You should support lazy loading and async decoding to improve page speed

In addition to these technical concerns I wanted a solution that was easy to implement and would support adding captions to my images. I also wanted all of these features to be available to use in both my Eleventy templates and plain markdown files.

I settled on using theses two plugins that take care of the heavy lifting:

  1. eleventy-img provides all of the responsive image markup
  2. markdown-it-image-figures adds optional caption support to Markdown images

Here's how I stung all the configuration together in my eleventy.config.js file:

const markdownItFigures = require('markdown-it-image-figures')
const pluginImage = require('@11ty/eleventy-img')

// Shared image configuration for templates & markdown
const image = {
  path: (src) => src.replace('/img', `${__dirname}/_includes/img`), // Source directory
  options: {
    widths: [686, 1576],
    formats: ['avif', 'jpeg'],
    outputDir: './_site/img' // Output directory
  },
  attrs: {
    sizes: 'calc(100vw - 2rem)',
    loading: 'lazy',
    decoding: 'async'
  }
}

// Add an Eleventy template shortcode
const responsiveImage = async function (src, alt = '', widths = null) {
  image.options.widths = widths || image.options.widths
  const metadata = await pluginImage(image.path(src), image.options)
  return pluginImage.generateHTML(metadata, {
    alt,
    ...image.attrs
  })
})
eleventyConfig.addAsyncShortcode('image', responsiveImage)

// Add markdown support
eleventyConfig.amendLibrary("md", markdown => {
  // Transform image markdown into responsive image markup
  markdown.renderer.rules.image = function (tokens, idx) {
    const token = tokens[idx]
    let src = image.path(token.attrGet('src'))
    const alt = token.content

    pluginImage(src, image.options)
    const metadata = pluginImage.statsSync(src, image.options)
    return pluginImage.generateHTML(metadata, {
      alt,
      ...image.attrs
    }, {
      whitespaceMode: "inline"
    })
  }

  // Add support for image captions
  markdown.use(markdownItFigures, {
    figcaption: 'title',
  })
})

This configuration assumes that you are storing your source images in _includes/img and that the resized and compressed images will be output to _sites/img. I wanted to author my image markup as if the image src was relative to the final output directory, so I included this line that rewrites the incoming image path to the correct source directory:

src.replace('/img', `${__dirname}/_includes/img`)

With all that setup out to the way, you can use images in Markdown like this:

![My image alt text](/img/my-image.jpg "My optional caption for this image.")

which will generate HTML like this:

<figure>
  <picture>
    <source type="image/avif" srcset="/img/vGjvxtLm9u-686.avif 686w, /img/vGjvxtLm9u-1576.avif 1576w" sizes="calc(100vw - 2rem)">
    <source type="image/jpeg" srcset="/img/vGjvxtLm9u-686.jpeg 686w, /img/vGjvxtLm9u-1576.jpeg 1576w" sizes="calc(100vw - 2rem)">
    <img alt="My image alt text" loading="lazy" decoding="async" src="/img/vGjvxtLm9u-686.jpeg" width="1576" height="1576">
  </picture>
  <figcaption>My optional caption for this image.</figcaption>
</figure>

...and the same image markup can be generated within an Eleventy template like this:

<figure>
  {% image "/img/my-image.jpg", "My image alt text", "calc(100vw - 2rem)" %}
  <figcaption>My optional caption for this image.</figcaption>
</figure>

Webmentions

No webmentions posted.