docs/developer/core-concepts/media.mdx
Spree handles uploads, processing, and delivery for product media. Images are automatically converted to WebP format and preprocessed into multiple sizes for optimal performance.
Product media uses the Spree::Asset model, which provides:
image, video, external_videoEach media item has a media_type field that defaults to image. The variant_ids field indicates which product variants the media is associated with — an empty array means the media belongs to the product as a whole.
When an image is uploaded, Spree automatically generates optimized versions in the background:
| Name | Dimensions | Use Case |
|---|---|---|
mini | 128x128 | Thumbnails, cart items |
small | 256x256 | Product listings, galleries |
medium | 400x400 | Product cards, category pages |
large | 720x720 | Product detail pages |
xlarge | 2000x2000 | Zoom, high-resolution displays |
All variants are cropped to fill the exact dimensions and converted to WebP format.
Every product response includes a thumbnail_url field — ready to use without any expands. Similarly, each variant includes a thumbnail URL and a media_count counter.
// List products — thumbnail_url is always included
const { data: products } = await client.products.list({ limit: 12 })
products.forEach(product => {
product.thumbnail_url // "https://cdn.../tote-front.webp" — no expand needed
})
# thumbnail_url is always in the response — no ?expand needed
curl 'https://api.mystore.com/api/v3/store/products?limit=12' \
-H 'Authorization: Bearer pk_xxx'
On the product detail page, expand media and variants to get the full set of media with all named variant URLs:
const product = await client.products.get('spree-tote', {
expand: ['media', 'variants'],
})
// Product media gallery
product.media // [{ id, media_type, product_id, variant_ids, original_url, mini_url, ..., alt, position }, ...]
// Each variant has its own thumbnail and media_count
product.variants?.forEach(variant => {
variant.thumbnail // "https://cdn.../tote-red.webp" — always available
variant.media_count // 3 — quick check without loading media
variant.media // full media array (only with ?expand=media)
})
curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=media,variants' \
-H 'Authorization: Bearer pk_xxx'
Response (media object):
{
"id": "media_k5nR8xLq",
"media_type": "image",
"product_id": "prod_86Rf07xd4z",
"variant_ids": ["variant_m3Rp9wXz"],
"position": 1,
"alt": "Front view",
"focal_point_x": null,
"focal_point_y": null,
"external_video_url": null,
"original_url": "https://cdn.example.com/images/original.jpg",
"mini_url": "https://cdn.example.com/images/mini.webp",
"small_url": "https://cdn.example.com/images/small.webp",
"medium_url": "https://cdn.example.com/images/medium.webp",
"large_url": "https://cdn.example.com/images/large.webp",
"xlarge_url": "https://cdn.example.com/images/xlarge.webp"
}
| Field | Available on | Always Returned | Description |
|---|---|---|---|
thumbnail_url | Product | Yes | URL to the product's first media |
thumbnail | Variant | Yes | URL to the variant's first media |
media_count | Variant | Yes | Number of media items (counter cache) |
media | Product, Variant | No | Full media array (requires ?expand=media) |
| Field | Type | Description |
|---|---|---|
id | string | Prefixed ID (media_xxx) |
media_type | string | image, video, or external_video |
product_id | string | null | Owning product prefixed ID |
variant_ids | string[] | Associated variant prefixed IDs (empty = product-level) |
position | number | Sort order |
alt | string | null | Alt text |
focal_point_x | number | null | Horizontal focal point (0.0–1.0) |
focal_point_y | number | null | Vertical focal point (0.0–1.0) |
external_video_url | string | null | External video URL (YouTube/Vimeo) |
original_url | string | null | Full-size image URL |
mini_url ... xlarge_url | string | null | Named variant URLs |
Spree uses libvips for image processing. Images are automatically:
Spree supports two storage service types:
| Service | Purpose | Examples |
|---|---|---|
| Public storage | Product images, logos, taxon images | S3 public bucket, CDN |
| Private storage | CSV exports, digital downloads | S3 private bucket |
thumbnail_url on listing pages — avoid loading full media via expandmini, small, medium, large, xlarge) for optimal performance