Images
An image field does not store a URL. It stores the image's media key in the store's own media:
Code
| Value | Meaning |
|---|---|
key | The image's media key: a path the platform chose under one of the store's folders (products/, landing/, reviews/); no leading slash, no .. |
alt | Alt text, a string or one per language; it reaches the component in the rendered language |
width, height | Optional intrinsic size in whole pixels, so the layout can reserve space |
focal | Optional focal point, { x, y }, each from 0 to 1 |
The URL is derived at render from the store's media delivery, so moving the
store's images to another host never rewrites a document. An image field has
no default: a theme cannot know a store's media, so
validateThemeDefinition refuses a default on one, and the component renders
its own fallback when nothing is picked.
The SDK never uploads. The platform places the file in the store's media and saves the key; the theme only reads that key.
The store's media delivery
config.media is the store's MediaDelivery, { kind, origin }: where every
key is served from (<origin>/<key>) and how that origin resizes.
kind | Resizing |
|---|---|
imagekit | ImageKit's ?tr= transformations |
cloudflare | Cloudflare's /cdn-cgi/image/…, with onerror=redirect, so a failed resize shows the original |
direct | None: images are served as stored |
A theme never names a host and needs no environment variable for images: the same build serves stores on all three kinds.
Resolve a key
A component receives the image with alt already in the rendered language.
pictureOf(image, delivery) from @wocart/storefront turns it into what an
<img> needs:
Code
pictureOfanswersnullwhen nothing is picked, the key is not a media key, or there is no delivery. Render the fallback then, never a broken<img>.picture.altis the resolved alt text,""when the merchant wrote none. Do not rebuild it from the language map.picture.positionis the focal point asobject-position(50% 30%), for an image cropped withobject-fit: cover.mediaImageUrlanswersnullwhen the delivery cannot resize (adirectdelivery): keeppicture.srcthen. The sizes, formats andsrcsethelpers are on Money, images, WhatsApp and Meta events.
The starter binds these helpers once, in src/lib/images.ts, and draws every
photo through src/components/ui/Photo.astro, which asks for a srcset of
widths so a phone downloads a phone-sized file.
Astro's <Image> on any media host
@wocart/theme/image-service adapts Astro's image pipeline to the store's
media delivery. Every <Image> and getImage() of a store image is resized by
the store's own resizer (ImageKit or Cloudflare), so a storefront on Cloudflare
and one on Vercel get the same URLs and no second image bill. Any other image
(another host, a file imported into the theme) is served as it is, and so is
every image of a direct delivery.
Astro asks the service for a URL without the request, so the service reads the delivery through a function you give it, on every image, never at build:
Code
Call keepStoreMedia(config.media) where the request's store is built, for
every config read from the server (not for the unavailable store, whose id
is ""). Before any delivery is known, every image passes through unchanged.
The starter does not use this service: it keeps Astro's passthrough service
and draws store images with the helpers above.
In astro.config.mjs, the entrypoint replaces the quick start's passthrough
service, on both hosts:
Code
| Adapter | Setting |
|---|---|
@astrojs/cloudflare | imageService: "custom" instead of "passthrough" |
@astrojs/vercel | Leave imageService off; the entrypoint is used |
Never fall back to Astro's default sharp service: it puts native binaries
for the build machine into the Vercel output, which cannot be deployed.
What the service does
mediaImageParams(options) maps Astro's transform to the resize parameters
mediaImageUrl takes:
| Astro option | Parameter |
|---|---|
width, height | width, height |
format | format |
quality (a number) | quality |
fit | cover, contain, scale-down, crop, pad and squeeze pass as they are; fill becomes squeeze |
mediaImageService(delivery) returns an Astro service whose getURL resolves
through mediaImageUrl and falls back to the original URL. It extends Astro's
base service, so <Image> and getImage() keep their usual behavior.
focal is not applied by the service: it is a hint for the theme's own CSS or
markup. width and height are the intrinsic values; a transform can still
request another size.
In a section
Code
A remote src goes through the configured service. An imported local image is
handled by Astro's base service as usual.
Rules
- Never store a URL in an image field. Store the key; resolve at render.
- Never build a media URL by string concatenation, and never name a media
host. Use
pictureOf,mediaUrl,mediaImageUrlor the service withconfig.media. - Never omit
alt. An empty alt is valid only for a decorative image. - Do not assume a format. A store may hold WebP, JPEG or another supported
type; ask for
format: "auto"and let the host choose. - Do not read the field as a string. It is an image reference,
{ key, alt?, width?, height?, focal? }.