Skip to content

Cloudflare image endpoint ignores Astro fit/position params for EmDash media transforms #2228

Description

@mvvmm

Summary

EmDash's Cloudflare _image endpoint wrapper (@emdash-cms/cloudflare/src/image-endpoint.ts) parses w, h, f, and q from Astro's image transform URLs but ignores fit and position. As a result, square/avatar crops served through the EmDash media pipeline don't get the same crop semantics as Astro's stock Cloudflare endpoint, and there's no way to opt into Cloudflare gravity: "face" for face-aware cropping.

Background

PR #1438 added responsive srcset for EmDash media via astro:assets. PR #1549 wrapped Astro's image endpoint so EmDash media bytes load from storage (Access-safe) instead of over HTTP. PR #2037 added a default quality of 85.

Astro's image service generates /_image URLs like:

/_image?href=/_emdash/api/media/file/{key}&w=32&h=32&q=60&f=webp&fit=cover&position=center

Astro's stock Cloudflare binding endpoint (@astrojs/cloudflare/dist/utils/image-binding-transform.js) already forwards fit to ImagesBinding.transform():

.transform({
  width: ...,
  height: ...,
  fit: url.searchParams.get("fit")
})

But EmDash's Cloudflare wrapper (@emdash-cms/cloudflare/src/image-endpoint.ts) only parses w/h/f/q via parseTransformParams and builds its own ImageTransform object without fit or position:

const transform: ImageTransform = {};
if (width) transform.width = width;
if (height) transform.height = height;
// no fit, no gravity

So when Astro requests a square 32×32 avatar with fit=cover, the Cloudflare Images binding receives width=32, height=32 but no fit — it defaults to scale-down (or the binding's default), which preserves aspect ratio and doesn't crop to the requested dimensions.

Impact

  • Avatars and square crops rendered from EmDash media on Cloudflare don't crop correctly — they get letterboxed/scale-down instead of cover-cropped.
  • DPR variance: DPR 1 screens fetch the 32w candidate (32×32, no fit), DPR 2 screens fetch 64w (64×64, no fit), producing inconsistent crop/quality across devices.
  • No face gravity: Cloudflare's ImagesBinding supports gravity: "face" for face-aware cropping, but there's no EmDash config to enable it. The images?: boolean integration option has no sub-config.
  • Astro position=center is always emitted for constrained images, so even if gravity were configurable, it would need to take precedence over the default position=center to take effect.

Proposed fix

1. Forward fit from _image URLs

Extend parseTransformParams in emdash/src/media/image-endpoint.ts to parse fit and position, and forward fit to ImagesBinding.transform() in @emdash-cms/cloudflare/src/image-endpoint.ts.

Astro's ImageFit values: fill | contain | cover | none | scale-down. Cloudflare's ImageTransform.fit accepts: scale-down | contain | pad | squeeze | cover | crop. Most map directly; fillsqueeze, none → omit.

2. Map position to Cloudflare gravity

Astro's position is a free string mirroring CSS object-position. Map only the subset Cloudflare supports: centercenter, toptop, bottombottom, leftleft, rightright. Ignore unsupported values (fall back to binding default).

3. Add configurable defaults via images option

Extend images?: boolean to images?: boolean | ImageConfig where:

interface ImageConfig {
  /** Default gravity for all EmDash media transforms. */
  gravity?: "face" | "center" | "auto" | "entropy" | "left" | "right" | "top" | "bottom";
  /** Default quality override (explicit `?q=` still wins). */
  quality?: number;
}

Preserve images: false (opt-out) and images: true (current default behavior).

When gravity: "face" is configured, it should override Astro's default position=center (since Astro always emits position=center for constrained images, a configured gravity would never take effect otherwise). An explicit non-center position from the URL should still take precedence over the configured gravity.

4. Tests

  • Unit test: parseTransformParams parses fit and position
  • Unit test: Cloudflare endpoint forwards fit to ImagesBinding.transform()
  • Unit test: configured gravity: "face" overrides default position=center
  • Unit test: explicit position=top overrides configured gravity

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions