
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:
npm install @11ty/eleventy-imgApproach 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:
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:
- Replace Markdown
<img>tags with placeholders during rendering - 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-imgoutput
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, '"')}" data-title="${title.replace(/"/g, '"')}"></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
- Quick copy‑paste shortcodes: Quick Start — Eleventy Shortcodes
This article is part of Quesby, an open-source boilerplate for modern static sites. See the GitHub repository.