Images & Thumbnails
Every image a template or island renders comes out of the filestore, and the filestore will derive a downscaled rendition on request. Ask for the size you actually draw. A URL with no size serves the file the user uploaded — which for product photography or a blog cover is routinely 2000px wide and two orders of magnitude more bytes than the slot on screen.
The size is part of the URL
Section titled “The size is part of the URL”Renditions are requested with ?s=<pixels>, where the number is the longest edge in real
pixels — not a CSS width. A 30px thumbnail on a 2x display needs ?s=64.
Only these sizes derive:
32 64 128 256 512 1024Anything else is served as the original. So never invent a number: pick one from the list, or use a helper below, which does it for you.
Renditions are WebP, generated on first request and cached on disk under the source file’s content hash. Replacing an image derives a fresh rendition automatically — there is no invalidation step, and nothing to purge.
Two formats deliberately have no renditions and always serve the original: SVG (already resolution-independent) and GIF (resizing would flatten the animation to one frame).
In templates
Section titled “In templates”Use the image and image_srcset filters. They are installed on every module template
environment alongside money, date and video, so a theme or app-store section gets them
without importing anything.
<img src="{{ product.image|image(512) }}" srcset="{{ product.image|image_srcset(256, 512, 1024) }}" sizes="(max-width: 767px) 50vw, (max-width: 1023px) 33vw, 25vw" alt="{{ t(product.name) }}" loading="lazy" decoding="async">Both filters accept an attachment id or an already-built URL, so the same expression works whether the render context handed you a raw field value or a resolved address.
{{ x|image }} with no argument is the original — correct for og:image and JSON-LD, where
crawlers want at least 1200px on the long edge and the URL is fetched by a scraper rather than
by every visitor.
sizes is not optional
Section titled “sizes is not optional”With a srcset and no sizes, the browser assumes the image fills the viewport and takes the
largest candidate every time — worse than shipping no srcset at all. Describe the CSS width
the image occupies at each breakpoint. If the element has a fixed pixel width, skip srcset
entirely and give image() that one size.
Lazy loading
Section titled “Lazy loading”Add loading="lazy" decoding="async" to anything below the fold. The one image that must
not be lazy is the page’s largest contentful paint — a product hero, a post cover. Mark
that one fetchpriority="high" instead; a lazy hero waits for layout before it starts
downloading and measurably delays the paint.
In islands
Section titled “In islands”window.fullfinity exposes the same two helpers:
const { imageUrl, imageSrcset } = window.fullfinity;
html`<img src=${imageUrl(line.image, 128)} width="64" height="64" loading="lazy" decoding="async" />`imageUrl(id, size) builds the address; imageSrcset(id, ...sizes) builds the candidate list.
Both accept an id or an existing URL. Do not concatenate /filestore/view/<id> by hand — that
is what leaves a 44px cart thumbnail downloading a full-resolution photograph.
When a route serializes images for an island to render, send the rendition the island will draw, plus its candidates:
from fullfinity.engine.utils import image_srcset, image_url
{ "image": image_url(product.image, 512), "image_srcset": image_srcset(product.image, 256, 512, 1024),}Caching
Section titled “Caching”A public attachment is served with a shared-cacheable Cache-Control, so a CDN or reverse
proxy in front of the site can serve it without reaching the origin. Access-controlled
attachments stay private.
When attachments are kept in object storage, the application answers these URLs with a
short-lived redirect to the file rather than sending the bytes itself, so images never travel
through a worker. The caching described here is unchanged — the policy is carried into the
redirect — and nothing in your templates changes: keep using the same helpers and the same
/filestore/view/<id> addresses.
A plain /filestore/view/<id> URL addresses a mutable record, so it gets a day of reuse
plus a week of background revalidation — long enough that a second page view is free, short
enough that a replaced image propagates on its own. A URL that also carries the content hash
(v=, minted by build_file_url) is immutable by construction and cached for a year.