Image and Media Helpers

There are three ways to put an <img> on a page from a template. Use Image or staticImage when you have the URL; FeatureImage when you want whatever the page’s own feature image is; ImageTag when something handed you an image-reference object, such as a menu item. Two more helpers give you the URL without the tag, one inlines an SVG partial so it can be themed, and one embeds the page’s feature video.

All eight are safe with plain {{ }}, which makes this the one group in this reference where you never need triple braces. All eight also assume the mechanics on How Helpers Work — in particular that the last argument is always the options object, which is why omitting a variant does not do what you would hope.

The variant model

A Cloudflare Images delivery URL ends in a variant name, and every image the CMS stores is delivered at original by default. “Setting a variant” in these helpers is not a transformation or a lookup. It is one line:

url.replace(DEFAULT_VARIANT, variant);   // DEFAULT_VARIANT === "original"

That has three consequences you can predict everything else from:

  • A URL that does not contain the string original comes back unchanged. Passing a variant to a URL that already carries one is a silent no-op — no warning, no error, the wrong size. If you are chaining helpers, start from the original URL each time.
  • Only the first occurrence is replaced. JavaScript’s String.replace with a string pattern is not global. A URL whose filename contains original will have that replaced instead of the variant segment.
  • Nothing validates the variant name. A misspelled variant produces a delivery URL for a variant that does not exist, which fails at request time in the browser rather than at build time.

Every variant

Ten variants exist. Six of them form the ladder the automatic responsive rewrite chooses between; the other four are fixed-purpose sizes that never appear in a srcset.

The ten variant names
VariantIn the responsive srcset?
originalYes
xxlYes
xlYes
largeYes
mediumYes
smallYes
socialNo
socialtinyNo
profileNo
profiletinyNo

These are the legal strings to pass to a helper. The pixel dimensions behind each one — and the rewrite that picks among the six — belong to Image Variants and Responsive Images.

Every tag these helpers emit carries data-responsiver="false", except the ones built from a CMS image reference. That attribute is the opt-out from the automatic srcset rewrite that runs over the finished HTML: you named a variant explicitly, so the rewrite leaves the tag alone. Image Variants and Responsive Images covers the other side of that contract.

Block image helpers: the body is the attribute list

Image, FeatureImage, staticImage and ImageTag share one pattern that looks strange the first time you see it. The block body is not content — it is the attribute list, rendered against the current context and interpolated into the tag:

{{#Image featureImage.src "medium"}}class="w-full rounded-xl" alt="{{ title }}"{{/Image}}
<img data-responsiver="false" src="https://imagedelivery.net/…/medium" class="w-full rounded-xl" alt="Team at work">

Because the body is a normal Handlebars block, anything on the context is available inside it — that is how alt="{{ title }}" works. Called without a block, each of these still renders; you just get a tag with no attributes beyond the ones the helper adds itself.

Image

Two parameters, both required: a URL and a variant name.

{{#Image image "profiletiny" }}class="h-10 w-10 rounded-full" alt="Profile picture of {{ fullName }}"{{/Image}}

Omitting the variant is the failure to watch for. The guard is if (variant == null || opts == null), and with one argument the options object lands in variant while opts is undefined — so the guard fires and you get an empty string plus one line in the build log:

[media] Too few parameters. {url, 'variant name'} are required.

An empty or whitespace-only URL is a softer failure — [media] Empty image specified. at warning level, and again an empty string.

FeatureImage

No parameters at all. It takes the page’s own feature image and renders it.

{{#FeatureImage}}class="w-full" title="{{ featureImage.alt }}"{{/FeatureImage}}
<img data-assetid="a1b2c3" src="https://imagedelivery.net/…/original" alt="Team at work" width="1280" height="720"  class="w-full" title="Team at work">

Two details in that output are worth knowing. data-responsiver="false" is absent — a CMS image reference is left to the responsive rewrite unless it is an SVG, which is the one case that gets the opt-out. And the alt text is HTML-escaped by the helper, so an alt containing a quote does not break the tag.

Finding the image is a two-step resolution: the helper unwraps the options object first, and falls back to unwrapping this. That is what makes the same call work on a single page and inside a paginated list item, where the item’s data is not on this. The attribute block, though, always renders against this — so if the two ever differ, an attribute referencing featureImage.alt reads the one on this, not the one that supplied the tag.

A page with no feature image is not an error. The helper returns an empty string and logs at debug level, which most builds do not show — so a missing image looks like a template that silently did nothing. Check the page’s own settings first; a feature image is chosen in the editor, at Page Settings.

ImageTag

Takes one argument: an image-reference object of the shape { assetId, src, alt, width, height, svg? }. Use it when something handed you an image rather than you knowing its URL — a menu item’s image is the shipped case:

{{ ImageTag item.image }}

It emits exactly what FeatureImage emits, because they share the same tag builder. It also works as a block, in which case the body is the attribute list as usual.

staticImage

For files you ship in your own repository rather than assets from the CMS — a logo, an icon, a diagram checked into src/static/images:

{{#staticImage "/static/images/logo.svg"}}class="h-6" alt="Logo" width="120" height="24"{{/staticImage}}
<img data-responsiver="false" src="/static/images/logo.svg" class="h-6" alt="Logo" width="120" height="24">

It takes no variant, because a repository file has none, and it never gets a srcset. Calling it with no URL logs [media] Too few parameters. url is required.; an empty URL logs [media] Empty image specified.

No shipped Leed template uses staticImage. That is not a warning — it exists for your templates, and Leed’s own happen to work exclusively from CMS assets.

URL-only helpers

imageUrl and absoluteImageUrl give you the URL and nothing else, for the places a tag would be wrong: a meta tag, a JSON feed, a CSS background. Both take a URL or an object with a .src property, plus a required variant.

{{ siteUrl }}{{ imageUrl image "profile" }}
<meta property="og:image" content="{{ absoluteImageUrl this "social" }}" />

The only difference is the prefix: absoluteImageUrl prepends https://<your domain> when the value does not already contain http, which is what makes it correct for a social card where a relative URL is useless. Note the order — it makes the URL absolute first, then swaps the variant.

Both require the variant. There is no parameter shift, and the options object is truthy, so {{ imageUrl image }} passes the guard and then stringifies the options object into the URL:

{{ imageUrl image }}
→ https://imagedelivery.net/…/[object Object]

Missing the URL as well logs [media] Too few parameters and returns an empty string.

svgDiagram — inlining a themeable SVG

svgDiagram renders one of your own SVG partials inline, wrapped in a themed container:

{{ svgDiagram "svg/charts/adoption" class="max-w-3xl mx-auto" }}
<div class="svg-diagram max-w-3xl mx-auto"><svg …>…</svg></div>

The parameter is a partial path relative to _includes, and class is a hash option appended to the wrapper. The current context is passed into the partial, so the SVG can read page data.

Inlining is the point. An SVG loaded through <img src="…svg"> is an opaque document: its fills cannot be restyled, so it cannot follow a light or dark color scheme. Inlined, every element is in the page’s DOM and reachable by CSS, which is what the shared stylesheet tailwind/site/svg-diagrams.css is for.

The partial name is validated against /^[\w/-]+$/ — letters, digits, underscore, slash and hyphen only. That is a security guard rather than tidiness: the name is interpolated straight into a partial-include expression which is then compiled, so an unrestricted name would let a data-derived value inject template syntax. A name that fails the test is refused with [media] svgDiagram: invalid partial name "…" and renders nothing. A name that passes but does not resolve to a partial logs [media] svgDiagram: failed to render partial "…" with the underlying message.

Like staticImage, no shipped Leed template calls it. It is there for your diagrams.

FeatureVideo — the Cloudflare Stream embed

No parameters. It reads the page’s featureVideo through the same two-step context resolution FeatureImage uses, and emits a Cloudflare Stream player:

{{ FeatureVideo }}
<div class="embedded-cf-video video-player">
  <iframe data-assetid="a1b2c3" data-cloudflare-video="true" src="https://customer-56fx8oxwdrk14ngj.cloudflarestream.com/9f2b…/iframe?muted=true&controls=true" allowfullscreen
    allow="accelerometer; gyroscope; encrypted-media; picture-in-picture; autoplay;"
    title="Cloudflare video player"></iframe>
</div>

That customer hostname is Leed’s Stream account and is the same on every site. It is not configurable, and it appears in the markup of every page that embeds a video, so there is nothing to hide — but do not treat it as a setting.

The block form parses and renders, but the block body is discarded. Unlike the four image helpers, there is no way to add attributes to the iframe. Everything that varies comes from the asset’s own options, set when the video was processed:

videoOptions keyEffect on the embed
autoplayAdds autoplay=true to the player URL, and keeps autoplay in the iframe’s allow list
loopAdds loop=<value> to the player URL
mutedAdds muted=<value>
controlsAdds controls=<value>
thumbnailTimestampAdds an encoded poster= pointing at the frame at that second
allowFullscreenAdds the allowfullscreen attribute to the iframe

A page with no feature video at all is fine — an empty string and a debug-level log line, exactly like FeatureImage. Playback options are properties of the asset, set at Video, Audio and Document Assets, not of the template.

Like staticImage and svgDiagram, no shipped Leed template calls FeatureVideo. Layouts that show a video call it themselves.

Reference

HelperFormParametersRequires on contextReturns / emitsBracesUsed by Leed’s own templates?
Imageblockurl string, required. variant string, requirednothing; the block body renders against thisSafeString — an <img> with data-responsiver="false"{{ }}Yes — PAGE_TYPE_INDEX.hbs
FeatureImageblocknonefeatureImage, on the unwrapped options or on thisSafeString — an <img> with data-assetid, escaped alt, width, height{{ }}Yes — PAGINATED_PAGE_TYPE_ITEM.hbs
ImageTagblock or inlineimage — an image-reference object, requirednothingSafeString, or "" silently for a non-object{{ }}Yes — leed/menu/link.hbs
staticImageblockurl string, requirednothingSafeString — an <img> with data-responsiver="false", never a srcset{{ }}No — yours to use
svgDiagraminline, plus a class hash optionname — a partial path relative to _includes, required, matching /^[\w/-]+$/passes this into the partialSafeString — a div.svg-diagram wrapping the inlined SVG{{ }}No — yours to use
FeatureVideoblock or inline; the body is ignorednonefeatureVideo, including a videoOptions objectSafeString — a wrapped Cloudflare Stream iframe{{ }}No — yours to use
imageUrlinlineurl — a string or an object with .src, required. variant string, requirednothingA plain URL string{{ }}Yes — leed-feed-json.hbs
absoluteImageUrlinlineSame as imageUrlnothingA plain URL string, origin-prefixed unless it already contains http{{ }}Yes — leed/metadata/ogCard.hbs

The six tag emitters return a Handlebars.SafeString, so their markup survives {{ }} unescaped. The two URL helpers return ordinary strings and are escaped like any other value — which is correct, because a URL going into an attribute should be. Either way, {{ }} is right for all eight, and {{{ }}} on any of them is a mistake waiting for a URL with an ampersand in it. The full escaping picture is on Helper Gotchas and Failures.

Authors writing page content rather than templates reach the same images through markdown instead, with captions, sizes and figure framing — see Images and Figures. The assets themselves, and the variants Cloudflare generates on upload, are covered at Uploading Images.

ESC