Image variants
Each theme declares the image variants (sizes / aspect ratios / formats) it expects. The admin's upload pipeline generates every declared variant whenever a user uploads an image — so when your template calls pickFormat(view, "small"), the file already exists at the expected URL.
Declaring variants
In manifest.ts:
imageFormats: {
inputFormats: [".jpg", ".jpeg", ".png", ".webp", ".gif"],
outputFormat: "webp",
quality: 80,
formats: {
small: { width: 480, height: 480, fit: "cover" },
medium: { width: 800, height: 800, fit: "cover" },
large: { width: 1600, height: 900, fit: "cover" },
portrait: { width: 800, height: 1200, fit: "cover" },
},
defaultFormat: "medium",
}
| Field | Purpose |
|---|---|
inputFormats | File extensions accepted by the upload picker |
outputFormat | What every variant gets encoded as — usually "webp" |
quality | WebP / JPEG encoding quality (1-100). 80 is a good balance |
formats | The named variants — keys are the names you'll pass to pickFormat(view, "<name>") |
defaultFormat | Fallback when pickFormat(view) is called without a specific name |
Format options
{
width: 800,
height: 800,
fit: "cover" | "contain" | "fill" | "inside" | "outside",
}
fit follows sharp's semantics, but the actual resize happens in the browser via canvas API — so the supported values are:
cover— crop to fill the box (most common for thumbnails)contain— fit inside the box, possibly with letterboxing- (Other modes are accepted but the browser-side implementation simplifies them to cover/contain)
Plus admin-only formats
In addition to the theme's declared formats, two admin-only variants are always generated:
admin-thumb— small thumbnail (~100×100) for the media library gridadmin-preview— medium preview (~400×400) for the media library detail view
These are added by the upload pipeline regardless of the theme's imageFormats, so the admin UI works the same on every theme.
Using variants in templates
import { pickFormat } from "@flexweg/cms-runtime";
<img
src={pickFormat(post.hero, "large")}
alt={post.hero.alt ?? ""}
width={post.hero.formats[post.hero.default].width}
height={post.hero.formats[post.hero.default].height}
/>
pickFormat(view, name) falls through:
- Requested format (
large) - Theme's
defaultFormat - Largest available variant
- Empty string
So a missing variant doesn't break — your template gets the closest available size.
For responsive images, list every variant explicitly:
<img
srcSet={`
${pickFormat(post.hero, "small")} 480w,
${pickFormat(post.hero, "medium")} 800w,
${pickFormat(post.hero, "large")} 1600w
`}
sizes="(max-width: 600px) 480px, (max-width: 1024px) 800px, 1600px"
src={pickFormat(post.hero, "medium")}
alt={post.hero.alt ?? ""}
/>
Storage layout
Each upload produces one folder per asset:
/media/
└── 2026/
└── 04/
└── launch-photo-1a2b3c/ ← random hex suffix prevents collisions
├── small.webp
├── medium.webp
├── large.webp
├── admin-thumb.webp
└── admin-preview.webp
The 6-character hex suffix is appended by normalizeMediaSlug() to every upload — collisions are impossible by construction.
When switching themes affects images
If the new theme declares a variant the old theme didn't have, existing uploads don't have that variant. The upload pipeline runs at upload time only — it doesn't retroactively regenerate.
pickFormat's fallback chain handles this gracefully:
- Theme A had
medium800×800 - You switch to Theme B which has
medium800×800 +portrait800×1200 - Existing images don't have
portrait—pickFormat(view, "portrait")falls back tomedium(square)
The image is the wrong aspect ratio but doesn't break. Re-upload affected images via the media library to generate the new variants.
There's no built-in "regenerate all image variants" tool because the original is discarded after upload — only the variants are kept. Mass regeneration would require keeping originals, which doubles storage and isn't worth it for most sites.
If you anticipate switching themes often, keep your high-res sources outside the CMS (Dropbox, NAS, etc.) and re-upload as needed.
Choosing variants
A few rules of thumb:
List the variants your templates actually use
Don't add a huge 4000×3000 variant "just in case." Each variant adds processing time + storage on every upload.
Cover the common breakpoints
Most sites need small for grids/cards, medium for in-content embeds, large for hero images. That's three variants.
Aspect ratio matters
A blog with portrait hero images needs a portrait variant. A square Instagram-style site might use square everywhere. The publisher won't crop intelligently — your fit: "cover" does, so pick aspect ratios that match your design.
WebP everywhere
Set outputFormat: "webp" in 99% of cases. WebP is supported by every browser at this point and produces files 25-35% smaller than JPEG at equivalent quality.
For one specific concern — printing or extreme accessibility tooling — you might want JPEG as well. But it's unusual.
Quality 80 is a good default
Below 70, you start seeing artefacts on detailed images. Above 90, file size grows fast for negligible visual gain. Photo-heavy sites might want 85; sites with mostly UI screenshots can go to 75.
Reference: built-in themes
| Theme | Variants |
|---|---|
| default | small (480² cover), medium (800² cover), large (1600×900 cover) |
| magazine | small, medium, large, plus portrait (800×1200 cover) for editorial heroes |
| corporate | small, medium, large — same as default |
Continue
- Theme manifest
- Templates and props — how templates use variants
- Reference: image formats — admin-side details