Skip to content

NEXT.JS

Cloudflare R2 Images in Next.js: One Hostname, Three Consumers

Cloudflare R2 images in Next.js need one hostname shared by article keys, the Image optimizer allowlist, and absolute social metadata URLs.

To serve Cloudflare R2 images in Next.js, keep the R2 hostname consistent in three places: the URL built from an article's object key, next/image's remotePatterns, and the absolute URL used by Open Graph and JSON-LD. A mismatch can leave the article route healthy while its image fails. This MDX blog setup supplies the article pipeline; here we inspect its image boundary on the current Next.js 15 site. An offline repository check passed 334 guards across 162 published articles on October 13, 2026.

Key takeaways

  • Content stores articles/<slug>/cover.webp, not a bucket hostname. One environment setting chooses the public image host.
  • Both the image URL builder and the next/image allowlist import lib/r2-hostname.mjs; the shared normalizer passed five input cases.
  • Page images can use a resolved URL, but social metadata and schema need an absolute URL.
  • A successful build does not fetch each cover. The site's cover check verifies both a local asset and a live R2 response.

The three URL consumers

The first consumer is the page. CoverPlaceholder and Figure call resolveImageSrc, which turns a bare R2 key into an HTTPS URL while leaving /public paths alone. The second is Next's image optimizer: it accepts only remote URLs matching images.remotePatterns. The third is metadata: an Open Graph image and JSON-LD image need a URL a crawler can fetch outside this page.

These consumers can disagree without a TypeScript error. The article renderer can construct https://new-host/articles/x/cover.webp while the optimizer still allows the old host. Or the page can show the cover while Open Graph resolves articles/x/cover.webp under the site domain. Our technical SEO guide covers metadata ownership; the R2 image path is a concrete case where that distinction matters.

Store keys in content

This site's frontmatter stores an R2 object key:

content/<category>/<slug>.mdx
cover: "articles/example/cover.webp"
coverAlt: "Measured result shown on the article cover"

lib/images.ts resolves bare keys against a configured R2 base URL. Root-relative paths remain /public assets, and absolute URLs pass through. That makes a CDN move a configuration change rather than a mass edit of MDX files. It also keeps article slugs aligned with their cover folders, which the publishing check can validate.

Cloudflare says its r2.dev public URL is for development; a custom domain is the production path and can use Cloudflare caching. The code's fallback hostname is a custom domain, but the actual public host can be set with NEXT_PUBLIC_R2_HOSTNAME.

Share the hostname with remotePatterns

next.config.mjs cannot import the TypeScript URL helper, so both files import the plain JavaScript lib/r2-hostname.mjs. The normalizer removes a pasted scheme, path, port, and case variation. Its five checked inputs include a blank value, a mixed-case https:// URL with a path, and a host with a port.

next.config.mjs
import { normalizeR2Hostname } from "./lib/r2-hostname.mjs";

const r2Hostname = normalizeR2Hostname(process.env.NEXT_PUBLIC_R2_HOSTNAME);

const nextConfig = {
  images: {
    formats: ["image/avif", "image/webp"],
    remotePatterns: [{ protocol: "https", hostname: r2Hostname, pathname: "/**" }],
  },
};

This is a focused excerpt of the site's config. Next.js documents that remote sources need remotePatterns; a mismatch can produce a 400 response from the optimizer. Remote images also need dimensions or a fill layout with sizes. Our components supply those inputs and a blur placeholder.

Use absolute URLs for social metadata

An article cover key is not a social image URL. In app/[category]/[slug]/page.tsx, absoluteImageUrl(meta.cover) supplies Open Graph metadata and the article schema. If the value is a /public path, the helper prefixes the site URL. If it is an R2 key, it returns the R2 URL. The page renderer uses resolveImageSrc because a relative /public path works inside the page; metadata leaves that page and needs an absolute address.

The key-to-URL conversion belongs in one helper. A second ad hoc conversion in the metadata route is how a CDN migration updates the visible cover but leaves social previews on an old host.

Verify the object after upload

The build does not establish that a remote object exists. On this site, an earlier audit found 12 article covers falling back to a placeholder: nine had correct keys whose objects were never uploaded, and three used frontmatter field names the loader did not read. The site added check:covers to catch both failure classes. The check validates the key and local file, then sends a HEAD request to every published cover and requires HTTP 200 with an image content type.

The new check:r2-image-route check complements that live check. It tests hostname normalization, asserts the URL builder and allowlist import the same module, checks the absolute metadata path, and validates every published cover key against a local file. Its October 13 run passed 334 guards for 162 published articles. This is source and local-asset evidence; the live-host evidence comes from check:covers.

Best practices

  • Keep a bare object key in MDX. It makes domain changes one-setting work.
  • Derive the allowlist and URL from one hostname. Two independent strings drift.
  • Provide dimensions and sizes. A responsive card should request a card-sized derivative.
  • Check the live object. The page build cannot tell you whether an upload was skipped.

Common mistakes

  • Allowing one host while rendering another. The R2 URL loads directly, but the Next optimizer rejects it. Compare the resolved source against remotePatterns.
  • Passing a key into Open Graph. The crawler resolves it under the site domain and may get a 404. Convert it to an absolute URL.
  • Using r2.dev as a production CDN. Cloudflare documents variable rate limits there. Attach a custom domain to the bucket.

Conclusion

Use R2 as the original image store and Next Image as the page optimizer, with one hostname feeding both. Store keys in content, convert them once for page rendering and once for absolute metadata, and verify each uploaded object independently of the build. The repository check makes the three consumers agree; the live cover check proves that the public URL answers. The MDX article setup guide shows where those keys enter the content pipeline.

Frequently asked questions

How do I use Cloudflare R2 images with next/image?
Expose the R2 bucket on a public custom domain, build an absolute HTTPS URL for each object key, and add that exact hostname to images.remotePatterns in next.config. Supply width and height for remote images or use fill with sizes. This site's URL builder and allowlist import the same hostname normalizer so a domain change cannot update only one side.
Why does an R2 image load directly but fail in Next.js Image?
The object may exist while the Next.js optimizer rejects its URL. Next Image requires a matching remotePatterns entry for an external source; a protocol, hostname, port, path, or query mismatch can return 400. Check the generated image URL, the configured hostname, and the exact pattern before investigating R2 permissions.
Should MDX store the full R2 URL for an article cover?
Store an object key such as articles/example/cover.webp and resolve it at render time. This keeps the CDN hostname in one setting and lets content survive a domain migration. The same key must become an absolute URL for Open Graph and schema output; leaving it relative to the site domain can point crawlers at a nonexistent route.
Does a successful Next.js build prove every R2 cover works?
No. The build reads frontmatter and renders routes without requesting each remote image. This site previously had covers with correct keys but missing R2 objects. Its cover check tests the local file and sends a HEAD request to every published cover, then fails on a non-200 response or a non-image content type.

Muhammad Kashif

Founder and editor of Devventa, covering AI coding assistants, Next.js and the modern AI development stack.