Skip to content

R2 storage publicUrl config is not used by Image component or media API endpoints #508

@devondragon

Description

@devondragon

Summary

The r2() storage config accepts a publicUrl option, but it has no effect on how images are served to visitors. The Image component and media API endpoints hardcode /_emdash/api/media/file/ instead of consulting the storage adapter's getPublicUrl() method.

This means all media requests go through the Worker (which reads from R2 and proxies the bytes), even when a public R2 custom domain is configured. The Cache-Control: public, max-age=31536000, immutable header mitigates this for repeat visits, but every cache miss still hits Worker CPU/memory to proxy R2 bytes.

Config

storage: r2({
  binding: "MEDIA",
  publicUrl: "https://media.example.com",  // accepted but unused
}),

Where it breaks

1. EmDashImage.astro (lines 58–64) — hardcodes the API route:

function buildLocalImageUrl(img: MediaValue): string {
  const storageKey = (img.meta?.storageKey as string) || img.id;
  if (storageKey) {
    return `/_emdash/api/media/file/${storageKey}`;
  }
  return "";
}

2. Portable Text Image.astro (line 128) — same hardcoded fallback:

src = asset.url || `/_emdash/api/media/file/${asset._ref}`;

3. Media API endpoints (api/media/[id]/confirm.ts:27, api/media.ts:35) — return hardcoded URLs instead of calling storage.getPublicUrl():

url: `/_emdash/api/media/file/${item.storageKey}`,

Where it works

The R2Storage class correctly implements it:

  • upload() returns url: this.getPublicUrl(options.key)
  • getPublicUrl() returns ${publicUrl}/${key} when configured ✅

But nothing downstream uses the upload result's URL or calls getPublicUrl().

Expected behavior

When publicUrl is configured, rendered <img> tags should use the public URL (e.g., https://media.example.com/storageKey.jpg) instead of /_emdash/api/media/file/storageKey.jpg. This would bypass the Worker entirely for media serving.

Suggested fix

Expose the storage's public URL base to the rendering layer — either:

  1. Make buildLocalImageUrl() in the Image components aware of the configured publicUrl, or
  2. Have the media API endpoints call storage.getPublicUrl(key) so the URL stored in content fields reflects the public URL

Option 2 would also fix existing content on next re-publish/re-save.

Environment

  • EmDash 0.1.x on Cloudflare Workers + R2
  • @emdash-cms/cloudflare storage adapter
  • R2 bucket with custom domain configured

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