# Images

> Understand and configure image optimization in deco.cx storefronts — components, quality settings, bypass rules, platform-specific handling, CDN proxy, and format negotiation. Use when the user asks about images, image quality, image optimization, or image components.

- Skill: `decocms/images` (Agent Skill)
- Install (CLI): `npx skillmds@latest add decocms/images`
- Raw SKILL.md: https://api.skillmd.com/api/skills/decocms/images/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: decocms (https://skillmd.com/u/decocms)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/decocms/images

---


# Images in deco.cx

Deco provides a full image optimization pipeline: components that generate responsive markup, a multi-engine server-side optimizer, platform-aware URL rewriting, and CDN caching with next-gen format negotiation.

---

## Image Components

### `Image`

**Import:** `apps/website/components/Image.tsx`

The primary image component. Wraps a standard `<img>` with automatic srcset generation, optimization URL rewriting, and optional preloading.

| Prop | Type | Default | Description |
|---|---|---|---|
| `src` | `string` | *(required)* | Image source URL |
| `width` | `number` | *(required)* | Display width — used for aspect ratio and srcset |
| `height` | `number` | — | Display height |
| `quality` | `QualityOptions` | context default | `"low"` (60) \| `"medium"` (70) \| `"high"` (80) \| `"original"` (100) |
| `fit` | `string` | — | `"contain"` or `"cover"` |
| `preload` | `boolean` | `false` | Adds `<link rel="preload">` for LCP optimization |
| `fetchPriority` | `string` | — | `"high"` \| `"low"` \| `"auto"` |
| `loading` | `string` | `"lazy"` | Native lazy loading attribute |

```tsx
import Image from "apps/website/components/Image.tsx";

<Image
  src="https://example.com/hero.jpg"
  width={1200}
  height={600}
  quality="high"
  preload
  fetchPriority="high"
  loading="eager"
/>
```

### `Picture` and `Source`

**Import:** `apps/website/components/Picture.tsx`

Use `Picture` with `Source` children for art-direction — serving different images or crops per breakpoint.

| Source Prop | Type | Description |
|---|---|---|
| `src` | `string` | Image source URL |
| `width` | `number` | Image width |
| `height` | `number` | Image height |
| `quality` | `QualityOptions` | Quality override |
| `media` | `string` | CSS media query (e.g. `"(max-width: 767px)"`) |
| `preload` | `boolean` | Enable preload for this source |

```tsx
import { Picture, Source } from "apps/website/components/Picture.tsx";

<Picture preload>
  <Source src="/mobile.jpg" width={360} height={480} media="(max-width: 767px)" />
  <Source src="/desktop.jpg" width={1200} height={600} media="(min-width: 768px)" />
  <img src="/desktop.jpg" width={1200} height={600} loading="eager" />
</Picture>
```

### `Video`

**Import:** `apps/website/components/Video.tsx`

Video element with optional image optimization via `forceOptimizedSrc`.

---

## Responsive Images (srcset)

The `Image` and `Source` components automatically generate a srcset with density descriptors.

- **Default factors:** `[1, 2]` — produces `1x` and `2x` variants
- When `preload` is enabled, only the highest factor (2x) is used for the preload link to optimize LCP
- Custom srcset can be passed via the `srcSet` prop to override automatic generation

---

## Default Image Quality

### Quality Levels

| Value | Encoded quality |
|---|---|
| `"low"` | 60 |
| `"medium"` | 70 |
| `"high"` | 80 |
| `"original"` | 100 |

### Setting a Site-Wide Default

Configure `defaultImageQuality` in the website app props. This sets the quality for every `Image`, `Source`, and `Picture` component that doesn't explicitly override it.

- Type: `DefaultQualityOptions` — allows `"low"`, `"medium"`, or `"high"` (excludes `"original"` to protect performance)
- Provided to components via `DefaultImageQualityContext` (React context)
- Any component can override the default with its own `quality` prop

**This is exposed as a CMS panel field, not just code.** In the CMS it appears under the **site config** (site settings → the `website` app) as **"Default Image Quality"** — *"The default quality for images when not explicitly set per component"* — a dropdown with `low` / `medium` / `high`. Anyone can change it from the UI without touching the repo.

> ⚠️ **Check this first when the whole site's images look soft/low-quality.** If it's set to **`low`** (60), *every* image that doesn't set its own `quality` prop is degraded site-wide — a silent, global quality regression that no per-component change will fix. It's easy to miss because nobody "changed the code."
>
> **Recommended: `high`** for storefronts where product/hero images matter (jewelry, fashion, etc.). `medium` (70) is an acceptable perf/quality balance; **`low` (60) is usually too aggressive** for a product catalog. Set it explicitly rather than relying on whatever the panel happens to hold.

> **Two different "blurry" causes — don't confuse them:**
> - **Soft across the *whole* site** → likely the **Default Image Quality** panel set to `low` (this section).
> - **Pixelated on *one* banner/section, worse on retina** → the **source image is smaller than the slot needs** (see *Diagnosing blurry / pixelated images* below). Quality won't fix that; you need a bigger source.

---

## Diagnosing blurry / pixelated images (undersized source)

The most common "distorted image" report is **not** stretching — it's a **source image with fewer pixels than the slot needs on retina (DPR2) screens**, so the pixels get upscaled and blur.

Tell-tale sign: **sharp when the file is opened directly, pixelated on the site.** (Opening shows the small native size; the site displays it larger.)

### Mechanism

1. The `Image`/`Source` declares a **base** `width` — the 1× size the slot occupies on a normal screen.
2. deco **auto-generates a 2× (and 3×) variant in the `srcset`** (see *Responsive Images* above). On a **DPR2** device, `width={700}` makes the browser fetch the **1400px** variant. (DPR = Device Pixel Ratio; "retina" = DPR ≥ 2; most phones and Macs are DPR2–3.)
3. If the uploaded source is smaller than that (e.g. 542px for a 1400px slot), the pixels are invented by upscaling ~2.6×. **No parameter fixes this** — `quality="original"` adds no detail that was never photographed, only bytes.

**Required source pixels = rendered CSS width × DPR.** A card that renders ~700px wide needs **1400px** on DPR2. Secondary effect: if the source aspect ratio ≠ the slot ratio, `fit="cover"` **crops** to fill (a slight crop people also read as "distorted").

### Diagnose

**1. Measure the real source** (not the served size) — request it **without** `width`/`height` so the proxy returns the original, then measure:

```bash
curl -sL "https://decoims.com/image?quality=original&src=<ENCODED_SRC>" -o /tmp/orig.webp
sips -g pixelWidth -g pixelHeight /tmp/orig.webp   # macOS; or: identify orig.webp
```

⚠️ If you leave `width=1400` in the URL, `naturalWidth` reports **1400** — the already-upscaled output, not the source. The real size only shows with the resize params removed.

**2. Find what the component asks for** — read the `Image`/`Source` `width`. The retina requirement is **`width × 2`**:

```tsx
<Source src={imageDesktop} width={700} height={342} media="(min-width: 1024px)" /> // → needs 1400×684
<Source src={imageMobile}  width={375} height={184} media="(max-width: 1023px)" /> // → needs 750×368
```

**Verdict:** source pixels ≥ `width × 2` → sharp. Source `<` `width × 2` → blurry (upscaled). **That's the bug.**

### Fix

**Upload a larger source.** Minimum = the slot's 2× size, in the slot's aspect ratio (bigger is fine — the proxy downscales, which never blurs; smaller is the bug).

- Desktop card ~700px wide → **1400×684** (≈2.05:1)
- Full-width mobile ~375px wide → **750×368** (or **700×700** if the mobile `Source` is square — see below)

Things that do **not** fix it: changing `width`/`height` (the on-screen slot is unchanged, the browser upscales anyway); `quality="original"` (no detail added — drop it to `"high"` to save bytes); CDN sharpening (deco/VTEX barely sharpens); shrinking the component in CSS (only helps if shrunk to ≤ source/2 CSS px, and it changes the design for *every* reuse of the component).

**No larger original?** Upscale with AI (e.g. **Upscayl**, free/local) — pick a scale that clears the target (3× on 542px → 1626px > 1400 ✓; 2× → 1084px < 1400 ✗). **Caveat:** AI invents detail — zoom in and verify fine features (gemstones, engraved text, logos) before shipping. A real higher-res photo always beats an upscale.

### Keep the admin guidance honest

deco renders a field's `@description` / `@title` JSDoc as the **CMS admin help text**. If it states a 1× size (e.g. `572x280`), editors upload undersized images. **State the 2× retina target:**

```tsx
interface CardImage {
  /** @description size Image 750x368 */   // mobile, 2×
  imageMobile: ImageWidget;
  /** @description size Image 1400x684 */   // desktop, 2×
  imageDesktop: ImageWidget;
}
```

### Mobile vs desktop: watch the per-breakpoint `Source`

A `Picture` has a separate `Source` per media query, each with its own `width`/`height` — so the **aspect ratio can differ between mobile and desktop**. A common bug: desktop is a wide rectangle (700×342, ~2:1) but mobile is **square** (350×350, 1:1), making mobile `fit="cover"`-crop the photo into a square (subject floating in whitespace). Decide the intended mobile shape by **looking at the live mobile render** (emulate DPR and screenshot), not by guessing from code, then make the `Source` and the uploaded image agree (rectangular → `375×184`, image 750×368; square → `350×350`, image 700×700).

### Plain-language explanation for a non-technical stakeholder

> The image looks pixelated because the uploaded file is smaller than the space it fills on the site. The banner needs an image of **1400×684 px** (and **750×368 px** for mobile), but the current one is only 542×280 px, so the site has to stretch it. It looks sharp when you open the file directly because there it shows at its small original size; on the site it's enlarged. Fix: upload the image at the larger size — ideally the original high-resolution photo.

---

## Image Optimization Pipeline

### Two-Tier System

Deco uses a two-tier optimization strategy:

1. **Platform-specific optimization** (default) — rewrites the URL using the e-commerce platform's native image CDN
2. **Deco optimization** — falls back to the deco CDN proxy when no platform matches

### Platform-Specific URL Rewriting

The component rewrites URLs based on the detected platform:

| Platform | URL strategy |
|---|---|
| **VNDA** | `/{width}x{height}{pathname}` |
| **Shopify** | Query params `?width=&height=&crop=center` |
| **VTEX** | Path format `{trueId}-{width}-{height}` |
| **Wake** | Query params `?w=&h=` |
| **Sourei** | Query params `?w=&h=&fit=&q=` |
| **Magento** | Query params `?width=&height=&canvas=&optimize=low&fit=` |

### Deco CDN Proxy

When no platform matches, the image URL is rewritten to pass through the deco CDN:

```
https://decoims.com/image?src={src}&width={w}&height={h}&quality={q}&fit={f}
```

- **Default host:** `https://decoims.com`
- **Override:** `DECO_CDN_HOST` environment variable or `window.DECO.featureFlags.cdnHost` browser flag

### Data URLs

Images with `src` starting with `data:` are returned as-is — no optimization is applied.

---

## Image Engines (Server-Side)

The image loader (`/live/invoke/website/loaders/image.ts`) picks an engine based on the runtime environment.

| Engine | When selected | How it works |
|---|---|---|
| **Pass-through** | `IMAGES_ENGINE=pass-through` or default | Returns the original URL with no processing |
| **WASM** | Worker available (Deno) | Server-side encoding via WebAssembly — supports JPEG, PNG, WebP, AVIF |
| **Cloudflare** | Running in Cloudflare Workers | Uses `cf.image` transform: `fetch(src, { cf: { image: { format, fit, width, height, quality } } })` |
| **Deco (ImageKit)** | Fallback | Proxies through ImageKit: `https://ik.imagekit.io/{id}/tr:w-{w},h-{h},q-{q}/{src}` |

### Environment Variables

| Variable | Values | Description |
|---|---|---|
| `IMAGES_ENGINE` | `pass-through` \| *(unset)* | Force a specific engine |
| `DECO_IK_ID` | string (default `"decocx"`) | ImageKit account ID for the Deco engine |

---

## Bypass Rules

### Bypass Platform Image Optimization

> **Removed in v1.152.0.** The `BYPASS_PLATFORM_IMAGE_OPTIMIZATION` env var and `window.DECO.featureFlags.bypassPlatformImageOptimization` browser flag were removed. Platform-specific URL rewriting is now always active when a platform is detected. If you are on a version older than 1.152.0, these flags still apply.

### Bypass Deco Image Optimization

Disables all optimization — returns original image URLs untouched.

- **Env var:** `BYPASS_DECO_IMAGE_OPTIMIZATION=true`
- **Browser flag:** `window.DECO.featureFlags.bypassDecoImageOptimization`

Use for debugging or when images are pre-optimized externally.

### Proxy Disable & Whitelist

Configured in the website app props (`mod.ts`):

- `disableProxy: true` — disables the image proxy entirely
- `whitelistURLs` — array of URL patterns; only matching sources are proxied (empty = allow all). Returns 403 for non-matching sources.

---

## Format Negotiation

The image loader negotiates the best format based on the browser's `Accept` header:

- **AVIF** — served when `image/avif` is in Accept (smallest file size, widest modern support)
- **WebP** — served when `image/webp` is in Accept
- **Original format** — fallback when neither is supported

Format detection also uses binary signature matching (byte-level) with fallback to `Content-Type` header.

Supported formats for processing: **JPEG, PNG, WebP, AVIF**.

---

## Caching

Optimized images are cached aggressively:

```
Cache-Control: public, s-maxage=15552000, max-age=15552000, immutable
```

This is ~6 months of immutable caching. The loader also uses the Cache API server-side to avoid re-processing identical requests.

Response headers include:
- `x-img-engine` — which engine processed the image
- `x-cache` — `HIT` or `MISS` for the server-side cache

---

## Performance Best Practices

1. **Preload LCP images** — set `preload` and `fetchPriority="high"` on the hero/banner image
2. **Set width and height** — prevents Cumulative Layout Shift (CLS)
3. **Use `Picture` for responsive art direction** — serve appropriately sized images per breakpoint
4. **Use `"high"` quality for hero images, `"medium"` or `"low"` for thumbnails** — balance visual quality vs file size
5. **Set a site-wide `defaultImageQuality` explicitly** — avoid forgetting quality on individual components. Prefer `"high"` for product/hero-heavy storefronts (`"medium"` is an acceptable balance). **Verify the CMS "Default Image Quality" panel isn't silently on `low`** — it degrades every non-overriding image site-wide
6. **Let lazy loading work** — only set `loading="eager"` on above-the-fold images
7. **Don't bypass optimization without reason** — the platform and deco CDN pipelines significantly reduce payload size

---

## Asset URL Prefixes

Before sending images to the CDN proxy, these known asset prefixes are stripped to hit storage directly:

- `https://decoims.com/`
- `https://storage.googleapis.com/deco-assets/`
- `https://assets.decocache.com/`
- `https://deco-sites-assets.s3.sa-east-1.amazonaws.com/`
- `https://data.decoassets.com/`

---

## Quick Reference — All Environment Variables & Feature Flags

| Variable / Flag | Type | Description |
|---|---|---|
| ~~`BYPASS_PLATFORM_IMAGE_OPTIMIZATION`~~ | env | **Removed in v1.152.0.** Previously skipped platform CDN to use deco proxy |
| `BYPASS_DECO_IMAGE_OPTIMIZATION` | env | Skip all optimization |
| `DECO_CDN_HOST` | env | Override CDN host (default `https://decoims.com`) |
| `IMAGES_ENGINE` | env | Force image engine (`pass-through`) |
| `DECO_IK_ID` | env | ImageKit account ID (default `decocx`) |
| ~~`window.DECO.featureFlags.bypassPlatformImageOptimization`~~ | browser | **Removed in v1.152.0.** Previously same as env, client-side |
| `window.DECO.featureFlags.bypassDecoImageOptimization` | browser | Same as env, client-side |
| `window.DECO.featureFlags.cdnHost` | browser | Override CDN host, client-side |

