Skip to main content

Image format catalog

The complete reference for how images flow through Flexweg CMS — from upload to public URL.

For theme-author guidance on declaring formats see Creating themes → Image variants. For the visitor-facing usage in editor blocks see Editor → Core blocks.

The pipeline​

User picks file in Media library
↓
Browser-side processing in services/imageProcessing.ts
↓
For each declared format:
• createImageBitmap(file)
• Canvas resize/crop per `fit`
• Encode as outputFormat (typically WebP) at quality
↓
For each variant: upload to media/<yyyy>/<mm>/<slug>-<hex>/<variant>.webp
↓
Firestore doc media/{id} written with formats map

Everything happens in the user's browser. No server, no third-party image API.

Where formats come from​

Three sources merge into the format catalog used per upload:

Active theme's imageFormats.formats​

The user-facing variants the theme expects. Declared in the theme's manifest:

imageFormats: {
inputFormats: [".jpg", ".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" },
},
defaultFormat: "medium",
}

Admin-only formats (always added)​

Used by the media library UI itself. Declared in src/services/imageFormats.ts:

NameSizePurpose
admin-thumb100×100 coverMedia library grid
admin-preview400×400 coverMedia library detail view

Built-in fallback (when no theme is active)​

If somehow no theme is active (very unusual), the upload pipeline uses a minimal fallback set with just a medium 800×800 format. The publisher won't actually be able to render anything in that state — this is mostly a safety net.

What gets generated​

For every uploaded image, every declared format runs through the resize pipeline. So a default theme upload generates:

small.webp (480×480)
medium.webp (800×800)
large.webp (1600×900)
admin-thumb.webp (100×100)
admin-preview.webp (400×400)

Five variants, all WebP. Stored under one folder per upload:

/media/2026/04/launch-photo-1a2b3c/
├── small.webp
├── medium.webp
├── large.webp
├── admin-thumb.webp
└── admin-preview.webp

The 6-character hex suffix (-1a2b3c above) is appended by normalizeMediaSlug() to make collisions impossible by construction. So two uploads of launch-photo.jpg get distinct folders.

Fit modes​

type ImageFit = "cover" | "contain";

Browser-side implementation:

  • cover — crop to fill the box. Image is scaled to be at least as wide AND tall as the target, then cropped from centre.
  • contain — fit inside the box. Image is scaled to fit within the target dimensions; aspect ratio preserved; possibly with letterboxing transparent pixels.

Sharp's other fit modes (inside, outside, fill) aren't supported in the browser implementation. If you need them, do server-side resizing externally before uploading.

Output formats​

outputFormat: "webp" | "jpeg" | "png"
  • WebP — recommended default. ~25-35% smaller than JPEG at equivalent visual quality. Universally supported.
  • JPEG — for photo-heavy sites where you want maximum compatibility. Larger files; no transparency.
  • PNG — lossless. Use only for logos / line art / images with sharp edges. Files are much larger than WebP/JPEG.

The quality field (1-100) maps to:

  • WebP: q=quality
  • JPEG: q=quality
  • PNG: ignored (always lossless)

What input formats are accepted​

inputFormats: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".svg", ".avif"]

The upload picker filters on these extensions. Browser support for processing (via createImageBitmap):

  • ✓ JPG, PNG, WebP, GIF — universal
  • ⚠ AVIF — Chrome 85+, Firefox 113+, Safari 16.4+
  • ⚠ SVG — passes through unchanged (rasterising via canvas would be lossy)

For SVG specifically: the pipeline detects the input is SVG and stores the original rather than rasterising. Templates can use the SVG variant directly.

The original is discarded​

After all variants are generated, the user's original file is NOT stored. Storage cost would double; very few use cases actually need the original.

Implications:

  • Switching themes that need a larger format than was generated: can't regenerate from the original. The pickFormat fallback chain (requested → default → largest available) handles it gracefully but you might end up with the wrong aspect ratio.
  • Re-cropping: not supported in the admin. Re-upload to get a fresh crop.

If you need to keep originals, store them outside the CMS (Dropbox, NAS) and re-upload as needed.

Picking variants in templates​

import { pickFormat, pickMediaUrl } from "@flexweg/cms-runtime";

// Specific variant with fallback chain:
<img src={pickFormat(view, "large")} />

// Or any reasonable URL (legacy-friendly):
<img src={pickMediaUrl(view)} />

// Responsive:
<img
srcSet={`
${pickFormat(view, "small")} 480w,
${pickFormat(view, "medium")} 800w,
${pickFormat(view, "large")} 1600w
`}
sizes="(max-width: 600px) 480px, (max-width: 1024px) 800px, 1600px"
src={pickFormat(view, "medium")}
/>

pickFormat's fallback chain: requested → view.default → largest available → empty string. So a missing variant doesn't break — your <img> gets the closest available size.

Theme migration scenarios​

Adding a new format to your theme​

After deploying the new theme version, existing media doesn't have the new variant. The upload pipeline only runs at upload time.

Workarounds:

  1. Re-upload affected images via the media library
  2. Templates use pickFormat — fallback chain handles missing variants

Changing dimensions of an existing format​

Old uploads have the OLD dimensions; new uploads have the NEW dimensions. There's no marker on the file itself indicating which version it is.

Workaround: rename the format. Old uploads keep the old name (large 1200×900), new uploads use the new name (large-2 1600×900). Templates pick large-2 first, fall back to large for legacy media.

Removing a format​

Old uploads have the variant — file is still there, takes storage. The theme's manifest no longer references it. The pickFormat helper still finds it as a fallback.

Continue​