Accepted
ADR 0049 — Image transformations URL contract
How the renderer composes Cloudflare Image Transformations URLs, and how it degrades to unoptimised originals where transforms are unavailable
Originals live in R2 and resizing is delegated to Cloudflare Image Transformations at the edge rather than pre-generating variants at upload time. This pins the URL shape the renderer emits, the signing posture, the cost ceiling, and the behaviour on hosts where transformations are unavailable.
The decision
Cusp serves resized image variants through Cloudflare Image Transformations at the edge rather than pre-generating them at upload time. The feature is off by default and opt-in through a single typed setting, so a fresh deploy serves unoptimised originals until the deployer enables it on a zone that supports it.
URL shape
The renderer composes a transform URL of the form /cdn-cgi/image/{options}/{source-url}, where the source is the URL of the R2 object, URL-encoded so a query string or comma in the source cannot bleed into the options segment. The single place that constructs these URLs is the transformedUrl helper in src/shared/render/image-transforms.ts; every consumer routes through it.
The canonical option list is width=W,quality=85,format=auto. Quality defaults to 85, matching Cloudflare's own default.
The responsive srcset widths are 400, 800, 1200, 1600, and 2400. The sizes attribute for inline editor-width images is (max-width: 768px) 100vw, 720px, matching the editor's content-width cap. Both are hard-coded for v1.
Unsigned URLs
Transform URLs are unsigned. Signing only matters if hotlink-driven cost amplification is a concern; with the free-tier ceiling and the immutable cache headers on the content-hashed R2 objects, unsigned is the right trade. The signing-key infrastructure used for theme and plugin packages can be repurposed if cost abuse is ever reported.
Settings-driven enablement
A typed-settings key, images.transform_enabled, drives the renderer. When true the renderer composes transformation URLs; when false or unset it serves the original. The default is false, so a fresh deploy never emits /cdn-cgi/image/... URLs the zone cannot serve. An administrator flips the setting on in admin settings once Image Transformations is enabled for the zone. There is no upload-time prompt nudging the deployer to turn it on; enablement is a deliberate settings action, and uploads while it is off simply serve unoptimised originals.
Why the free-tier posture
Image Transformations is free for 5,000 unique transforms per month, then $0.50 per 1,000. For a personal site at a handful of images per page that ceiling is generous. A high-traffic site that exceeds it gets a modest Cloudflare bill, and the paid Cloudflare Images product is a one-click escape. A typical published page stays well under the ceiling because the srcset is capped at five widths per format.
The workers.dev fallback
Image Transformations is a zone-level feature, and the shared *.workers.dev zone has it disabled. A workers.dev deploy serves unoptimised originals: images.transform_enabled stays false, so the renderer emits the source URL directly. Once the deployer points a custom domain and enables Image Transformations for it in the Cloudflare dashboard, they turn the setting on. Editors see images either way — the public site is never broken.
Consequences
- Originals are the single source of truth in R2. There is no per-variant storage cost; re-encoding to AVIF or WebP is delegated to the edge on first request and cached.
- The immutable cache header on the R2 object is inherited by the transformation layer. A re-publish that swaps the media reference on a block emits a new URL, and the old variant ages out naturally.
- The renderer emits a picture element with AVIF, then WebP, then a default-format img fallback. The ordering is hard-coded in the default theme's template; a theme that needs a different order overrides the template function.