Image shortcode
The shortcode is Baseline's; the underlying engine is @11ty/eleventy-img. The two halves split cleanly:
- Baseline owns the shortcode call signature (
{% image { src, alt, ... } %}), the default widths and format order, the filename convention, the on-request optimisation in dev, the<picture>and<figure>HTML assembly, and the cohabitation rule witheleventyImageTransformPlugin. - eleventy-img owns the actual image transcoding, format conversion, content-hash digest, srcset emission, and remote-image fetching.
Anything you can configure on @11ty/eleventy-img directly, you can pass through the shortcode's options. Anything that is purely about the rendered HTML or the conventions Baseline applies on top is owned here.
If your project already defines its own image shortcode, Baseline leaves it alone and says so at the start of the build. Yours wins; there is no flag to set.
Usage
{% image {
src: "/media/mountains.jpg",
alt: "Mountains at sunset",
caption: "Responsive image via baseline",
widths: [320, 640, 960],
formats: ["avif", "webp", "jpeg"]
} %}
What it does
- Generates multiple formats and widths and returns a
<picture>containing one<source>per format and a single<img>fallback. Wraps in<figure>when a caption is provided. - Uses on-request transforms during
ELEVENTY_RUN_MODE=serve(via eleventy-img'stransformOnRequest) to keep dev startup quick. During the content-graph pre-pass it usesstatsOnly, which skips the encode the same way but keeps the real hashed URLs, so the markup the graph reads matches the built site. Either way, if the call fails it retries without the flag and logs a warning. In a real build with neither flag, errors throw immediately. - Adds the
eleventy:ignoreattribute on<img>wheneleventyImageTransformPluginis registered, so the transform plugin doesn't reprocess the shortcode's output. Both can coexist in the same project; the shortcode owns explicit calls, the transform plugin owns content-level rewrites.
The Image shortcode Shape
{% image {
src: "/media/example.jpg",
alt: "Description",
caption: "",
loading: "lazy",
img: {},
picture: {},
widths: [320, 640, 960, 1280, 1920],
sizes: "(max-width: 768px) 100vw, 768px",
formats: ["avif", "webp", "jpeg"],
outputDir: "./dist/media/",
urlPath: "/media/",
setDimensions: true
} %}
Every key shown with its default, with two that are shown as what you get rather than as what is set. outputDir is the place the files end up, which is not always the place they are written; see Caching. sizes is emitted with auto, in front of it on lazy images, and the value above is what sits behind that; see How sizes is decided. src and alt are required; the rest are optional.
Required arguments
src- local path or remote URL. Throws if missing. Local paths are resolved againsteleventyConfig.directories.input.alt- required, and a missing one throws. Use an empty string (alt: "") for purely decorative images: that is a different statement from having noaltat all, and both reach the output intact.
Defaults
- Widths:
[320, 640, 960, 1280, 1920]. No'auto': that value re-encodes the full-size original, which is the most expensive rendition in the set and rarely the one you want. Add it towidthswhen you do. Nothing is upscaled, so a width above your source's own is capped rather than wasted. - Formats:
['avif', 'webp', 'jpeg'](order matters for<source>negotiation; the<img>fallback picks by compatibility instead, see below).jpegis last and costs a third encode per width. It is there because avif and webp can both be refused, and the fallback has to be readable by whoever fell back to it. - Output:
urlPath: '/media/'. In a build, renditions are written to.cache/media/and copied into the output afterwards, so adistwipe does not cost you a re-encode. See Caching. In--servethey go straight to the output directory. Name anoutputDiryourself and it is used as written in either case. - Filename:
name-hash-widthw.format(e.g.hero-a1b2c3-640w.avif). The hash is the first six characters of eleventy-img's content digest, so different sources sharing a basename do not collide. - Caption: off unless
captionis set; when set, wraps in<figure>. - Dimensions:
widthandheightare written onto<img>unlesssetDimensions: false.
Common options
caption,loading('lazy'or'eager')widths,formats,sizesoutputDir,urlPathimg- any attributes for the<img>element,classandstyleincluded. What you pass wins over what the shortcode worked out, soimg: { loading: "eager" }andimg: { width: 100 }both do what they look like.picture- the same for the<picture>element. Any attribute, not just a class, sopicture: { slot: "image" }works for a component that wants a named slot.setDimensions(defaulttrue) - set tofalseto omitwidth/heighton the<img>.
Attributes: name the element
There is one rule for getting attributes onto the output. img is a bag of attributes for the <img>, picture is a bag for the <picture>, and both take anything HTML takes.
{% image {
src: "/media/hero.jpg",
alt: "The valley at first light",
img: { class: "u-rounded", "data-zoom": true },
picture: { class: "c-hero__media", slot: "image" }
} %}
Nothing merges and nothing silently wins. A value you pass beats one the shortcode worked out for itself.
If you used the older options
imageClass, containerClass, style, attrs and figure are gone rather than deprecated. Passing one is ignored, so check your call sites.
| was | now |
|---|---|
imageClass: "a" |
img: { class: "a" } |
attrs: { "data-x": 1 } |
img: { "data-x": 1 } |
style: "color: red" |
img: { style: "color: red" } |
containerClass: "b" |
picture: { class: "b" } |
figure: false |
nothing; omit caption |
figure is the one with no replacement, and dropping it fixes a bug rather than just tidying: figure: false alongside a caption used to return the picture on its own and discard the caption, silently, on a green build. A caption is now the only thing that decides whether the <figure> appears, so setting one always means you get it.
Notes
Generated URLs are site-root-relative (/media/...). The shortcode does not prefix output paths with the site URL, so keep settings.url set if you need absolute URLs for canonical tags or OpenGraph metadata.
The shortcode and eleventyImageTransformPlugin coexist. When both are active the shortcode adds eleventy:ignore to its own <img> so the transform does not reprocess it, which leaves you free to use whichever fits the page: the shortcode for explicit calls with control over widths and formats, the transform for rewriting <img> tags written into Markdown.
How sizes is decided
Any fixed sizes string is an assertion about a layout the shortcode cannot see, so on a lazy image Baseline asks the browser instead: sizes="auto, (max-width: 768px) 100vw, 768px". A browser that supports auto uses the width the image is really laid out at. auto is only valid where a lazy <img> follows the <source>, so an eager image gets the string alone.
The (max-width: 768px) 100vw, 768px behind it is a fallback, not a considered default. It is only read by browsers that cannot resolve auto, and it is there because a bounded guess serves them better than 100vw, which would have every one of them reach for the largest candidate under the viewport width. It is not a claim about your layout, and if it does not suit yours, pass your own.
Pass sizes yourself and it is used verbatim, auto included or not. The guess is Baseline's to improve; your answer is not.
Which rendition the <img> gets
The <img> inside <picture> is what a client sees when it can use no <source> at all, so it takes the most compatible format available rather than the first one you listed: jpeg, then png, gif, svg, webp, avif. With the default formats that means the <img> is a jpeg, while all three still get their own <source> in the order you asked for. If you generate a format outside that list it is used as-is.
Within the chosen format it takes the largest rendition, and the width and height describe that same file. There is no srcset on the <img>, so src is not the first of several candidates a client picks from: it is the whole answer. Browsers never see it, since they read the <source> list. What reads it is everything that parses the HTML without rendering it, and a feed reader or a link-preview bot handed the smallest rendition ships a thumbnail where the page shows a photograph.
The retry, and what its warning means
The shortcode calls Image() with a flag that skips the encode (transformOnRequest in dev, statsOnly during the pre-pass) and falls back to a call without it if that throws. The retry is unguarded: if the second attempt also fails, on a bad src or a missing file, the error surfaces. The warning names whichever flag was on, so read it as "the first attempt failed" rather than as a diagnosis. The real cause may be elsewhere.
Caching
Encoding an image is the slowest thing in most builds, and nothing about a rendition changes between builds. So in a build the shortcode writes its output to .cache/media/ and copies it into dist/media/ afterwards. Wipe dist, or let a CI runner start from a clean checkout, and the renditions are still there.
.cache/media/, not .cache/@11ty/img/, which is where the eleventy-img recipe puts them. The cache belongs to Baseline rather than to the library encoding the bytes, and the path mirrors dist/media/ and the /media/ URL so the folder looks like what it holds.
This only happens in a build. --serve generates on request through the dev middleware, and the content-graph pre-pass skips the encode entirely, so neither has bytes worth keeping. Name an outputDir yourself and Baseline leaves it alone: redirecting our own default is fair, redirecting your answer is not.
Keeping it between deploys
Locally the cache survives on its own. On a host it survives if the host is told to keep it. Vercel and Cloudflare Pages persist the build cache without asking. Netlify needs one plugin, and it takes two steps rather than one.
Install it, so Netlify has something to load:
npm install --save-dev netlify-plugin-cache
Then declare it and name the path:
[[plugins]]
package = "netlify-plugin-cache"
[plugins.inputs]
paths = [ ".cache/media" ]
Both halves are needed. A [[plugins]] entry naming a package that is not in your package.json fails the build rather than being skipped, and the error names the plugin rather than the missing install.
Name .cache/media rather than .cache. The content graph is rebuilt from scratch on every run, so restoring the previous one saves nothing and only makes the cache artefact bigger. The renditions are the expensive part, and they are the part that does not change.
If you want to see whether it is working, netlify-plugin-debug-cache lists what was restored on each deploy. It takes no inputs, so it is a second [[plugins]] block naming the package and nothing else.
Do not tidy the filenames
hero-a1b2c3-640w.avif looks like it has noise in the middle, and the noise is what makes the cache safe. The only check anywhere is whether a file of that name already exists, and the hash is taken from the image's own bytes plus the encode options. Replace the image, and the name changes, and the old rendition is never asked for again.
Take the hash out with a custom filenameFormat and you get the opposite: the new image keeps the old name, the stale file answers the existence check, and the old picture ships. With the cache persisted across deploys, it ships until somebody clears the cache by hand.
When to clear it
Nothing here expires. A rendition that exists is a rendition that is still correct, so the cache is only ever added to, and clearing it is maintenance rather than something the plugin decides for you.
rm -rf .cache is the reset. Reach for it if dist/media/ accumulates renditions no page uses any more, after deleting images or changing widths, or if a build was interrupted mid-encode: that is the one failure this design cannot see, since a truncated file has a valid name and now persists instead of being cleaned away with dist.
Changing the defaults for a whole project
Set them once at registration rather than on every call. Dropping avif, as below, is the usual reason: it is the slowest of the three to encode, and the other two cover every client between them.
eleventyConfig.addPlugin(
baseline(settings, {
media: { image: { widths: [640, 960, 1440], formats: ['webp', 'jpeg'] } }
})
);
Any single {% image %} still overrides them, so the three levels are: what the call says, then what the project set, then Baseline's fallback. The resolved values are on _baseline.options.media.image if a template needs to read them.
See also
- Site settings -
settings.urlfor absolute URL contexts. - Plugin entrypoint -
eleventyImageTransformPluginis opt-in. - Assets module - for static assets the shortcode does not own.