Pick the R2 public domain once; URLs get baked into content

Absolute R2 URLs end up inside stored article bodies and third-party CMSs; renaming the bucket's custom domain later breaks every image already published.

KV/R2/D1

· Chapter

7

·

2

min read

The answer. Put a custom domain on the R2 bucket from day one, pin it as a Worker var, and treat both as immutable. The moment a content pipeline writes <img src="https://media.example.com/..."> into an article body, that URL is data, not config: it lives in your database, in the customer's CMS, and in whatever third party re-hosts the body. You can't rotate it with a redeploy. A rename or an outage of that hostname breaks in-body images across every article you have ever published.

The pattern.

# wrangler.toml — the bucket and its public base travel together
[env.prod]
vars = { MEDIA_PUBLIC_BASE = "https://media.example.com" }   # chosen ONCE

[[env.prod.r2_buckets]]
binding = "MEDIA"
bucket_name = "my-media"   # custom domain attached to this bucket in the dashboard
// Refuse to write content until BOTH halves are bound.
if (!env.MEDIA || !env.MEDIA_PUBLIC_BASE) {
  return { skipped: "image_sink_media_unbound" };   // inert, not broken
}
await env.MEDIA.put(key, bytes, { httpMetadata: { contentType } });
const url = `${env.MEDIA_PUBLIC_BASE}/${key}`;     // this string is now permanent

The receipt. HarperFlow's API Worker started writing generated featured, OG and body images to R2 for the care-package delivery sink (2026-08-09) and the Shopify sink (2026-08-12). Shopify re-hosts the featured image but not body <img> tags, so every live Shopify article hotlinks our media hostname directly from the public storefront. The comment above the binding in wrangler.toml reads "chosen ONCE ... must NEVER change" for exactly that reason. The binding and the var together lift an inert-guard in dispatch: if either is missing, image generation is skipped rather than producing broken links. We didn't count how many articles carry baked URLs; the number only goes up.

Watch out.

  • The r2_buckets block is config-as-code, not a secret. Add it only after the bucket exists, or the deploy fails.
  • Don't put the default r2.dev URL in content; it is rate-limited and meant for development.
  • Set cache headers on the objects. The domain is forever; the bytes don't have to be.

Related: wrangler-kv-r2-default-local-use-remote · webflow-homepage-via-transform-rule-rewrite