Advanced: Integrate eleventy-img with the Markdown Pipeline

Advanced: Integrate eleventy-img with the Markdown Pipeline

Advanced guide to automatically optimize Markdown images with @11ty/eleventy-img using a Markdown renderer override and an async HTML transform. Includes comparison with shortcodes and cross-links.

When you build a site with Eleventy, images can quickly become a performance bottleneck. Visitors expect sharp visuals that load fast on every device, and modern formats like AVIF and WebP can make a huge difference. The problem is: managing responsive images by hand is messy.

This is where eleventy-img comes in. With a small amount of setup, you can automatically generate multiple sizes and formats, then drop them into your pages using a simple shortcode—or even process standard Markdown images automatically.
Looking for a copy‑paste setup with ready‑to‑use shortcodes? See the quick start:

In this guide, we’ll walk through a complete setup that covers both approaches.


Prerequisites

Before starting, you should have:

  • An Eleventy project up and running
  • Node.js and npm installed
  • A basic understanding of Nunjucks templates and Markdown

Setting Up eleventy-img

First install the package:

Terminal window
npm install @11ty/eleventy-img

Approach 1: A Custom Nunjucks Shortcode

Shortcodes are a clean way to embed optimized images directly in your templates. Create a file called src/eleventy/shortcodes.js and add the following:

import path from "node:path";
import Image from "@11ty/eleventy-img";
async function imageShortcode(src, alt, sizes = "100vw") {
if (!alt) throw new Error(`Missing alt for ${src}`);
const resolved = path.resolve("src/assets/images", src.replace(/^\/?src\/assets\/images\/?/, ""));
const metadata = await Image(resolved, {
widths: [320, 640, 960, 1280, null],
formats: ["avif", "webp"],
outputDir: "./_site/img/",
urlPath: "/img/"
});
return `<figure>${Image.generateHTML(metadata, {
alt,
sizes,
loading: "lazy",
decoding: "async"
})}</figure>`;
}
export default (cfg) => {
cfg.addNunjucksAsyncShortcode("image", imageShortcode);
};

Then import it in your eleventy.config.js:

import shortcodes from "./src/eleventy/shortcodes.js";
export default function(eleventyConfig) {
shortcodes(eleventyConfig);
}

From now on you can write:

{% image "src/assets/images/hero.jpg", "Homepage hero image" %}

…and Eleventy will output a <picture> element with AVIF, WebP, and all the necessary widths.


Approach 2: Automatically Handling Markdown Images

Markdown images like:

![Screenshot](/assets/images/app.png)

are convenient, but Eleventy doesn’t optimize them by default. To fix this, we can extend the Markdown pipeline so every image goes through eleventy-img behind the scenes. The idea is:

  1. Replace Markdown <img> tags with placeholders during rendering
  2. After Eleventy generates the HTML, run a transform that swaps those placeholders with fully optimized <picture> elements

This takes a bit more code, but the result is seamless: authors keep using plain Markdown, while the build process ensures images are always responsive and optimized.

Implementing the Markdown pipeline integration

We will:

  • Override the Markdown renderer for images to output a lightweight placeholder element
  • Add an async HTML transform that finds those placeholders and replaces them with eleventy-img output

Create eleventy/markdown-images/index.js:

import path from "node:path";
import Image from "@11ty/eleventy-img";
function installMarkdownPlaceholder(eleventyConfig) {
eleventyConfig.amendLibrary("md", (mdLib) => {
const defaultImageRule = mdLib.renderer.rules.image;
mdLib.renderer.rules.image = (tokens, idx, options, env, self) => {
const token = tokens[idx];
const src = token.attrGet("src") || "";
const alt = token.content || token.attrGet("alt") || "";
const title = token.attrGet("title") || "";
// Produce a placeholder element that survives HTML serialization
return `<md-img data-src="${src}" data-alt="${alt.replace(/"/g, '&quot;')}" data-title="${title.replace(/"/g, '&quot;')}"></md-img>`;
};
});
eleventyConfig.addTransform("md-img-to-picture", async (content, outputPath) => {
if (!outputPath || !outputPath.endsWith(".html")) return content;
const replaceAsync = async (str, regex, asyncFn) => {
const promises = [];
str.replace(regex, (match, ...args) => {
const promise = asyncFn(match, ...args);
promises.push(promise);
return match;
});
const data = await Promise.all(promises);
let i = 0;
return str.replace(regex, () => data[i++]);
};
const html = await replaceAsync(
content,
/<md-img[^>]*data-src="([^"]+)"[^>]*data-alt="([^"]*)"[^>]*data-title="([^"]*)"[^>]*><\/md-img>/g,
async (_match, src, alt) => {
const normalized = src.replace(/^\/?src\/assets\/images\/?/, "");
const resolved = path.resolve("src/assets/images", normalized);
const metadata = await Image(resolved, {
widths: [320, 640, 960, 1280, null],
formats: ["avif", "webp"],
outputDir: "./_site/img/",
urlPath: "/img/",
});
return `<figure>${Image.generateHTML(metadata, {
alt,
sizes: "100vw",
loading: "lazy",
decoding: "async",
})}</figure>`;
}
);
return html;
});
}
export default (cfg) => {
installMarkdownPlaceholder(cfg);
};

Register in eleventy.config.js:

import markdownImages from "./eleventy/markdown-images/index.js";
export default function(eleventyConfig) {
eleventyConfig.addPlugin(markdownImages);
}

Now authors can keep writing plain Markdown images, and the build will output fully optimized <picture> elements.


Putting It All Together

With this setup:

  • Nunjucks shortcodes are available when you want fine-grained control.
  • Plain Markdown images also get optimized automatically.

The end result looks like this:

<figure>
<picture>
<source srcset="/img/example-320.avif 320w, /img/example-640.avif 640w, ..." type="image/avif">
<source srcset="/img/example-320.webp 320w, /img/example-640.webp 640w, ..." type="image/webp">
<img src="/img/example-1280.webp" alt="Screenshot" sizes="100vw" loading="lazy" decoding="async">
</picture>
</figure>

Why Bother?

  • Performance: AVIF and WebP drastically reduce file size without sacrificing quality
  • Responsive design: multiple widths ensure the browser always picks the right size
  • Accessibility: alt text is preserved everywhere
  • Convenience: authors can keep writing Markdown as usual

Shortcodes vs Markdown pipeline

Choose based on authoring needs:

  • Shortcodes: maximum control per template, easy to pass custom sizes, classes, and wrappers
  • Markdown pipeline: hands‑off authoring for content teams, consistent output across the site

You can also combine them: use the Markdown pipeline for most content and fall back to shortcodes where you need precise control.


Wrapping Up

By combining a Nunjucks shortcode with a Markdown transform, you get the best of both worlds: flexibility for custom templates and simplicity for everyday content.

This approach is a solid starting point for any Eleventy project that takes performance seriously. And once you’ve set it up, you’ll never need to worry about hand-crafting <picture> tags again.

Next steps


This article is part of Quesby, an open-source boilerplate for modern static sites. See the GitHub repository.