Reliable Image Delivery with Next.js 16 and vinext: Cloudflare R2, Caching, and Markdown
# A practical image-delivery design for Next.js 16 and vinext: store objects in R2, keep URLs in D1, and verify Markdown, caching, and production rendering.
The difficult part of an image-enabled CMS is not the upload itself. The real work is keeping URLs stable after publication, preventing layout shifts across screen sizes, and ensuring an image failure never takes the article body down with it.
This guide uses a Next.js 16 + vinext blog deployed to Cloudflare Workers. D1 stores article text and image URLs, while R2 stores the image objects. The focus is not only implementation, but also failure boundaries and production verification.
Decide five constraints first
Before implementing uploads, establish these rules:
• Do not store image binaries and article records in the same persistence layer.
• Do not overwrite a published image at the same immutable URL.
• Validate MIME type and size on the server.
• Store only the image URL and meaningful alt text in Markdown.
• Define success as “visible on the published article,” not merely “the save API returned 200.”
A D1 BLOB can technically hold an image, but that mixes backup, transfer, and caching concerns with searchable article data. D1 is better used for search, ordering, and concurrency control, while object storage handles binary delivery.
Choose among three storage strategies
Put assets in `public/`
This is the simplest option for editorial diagrams that are known at build time. Git records every change and the assets ship with the deployment. It is a poor fit for ongoing CMS uploads because every new file requires another build.
Store uploads in R2
R2 fits images added from an admin console. A Worker binding can write the object directly and the article only needs the resulting URL. If every upload receives a new key containing a UUID or content hash, the response can safely use a long-lived header such as Cache-Control: public, max-age=31536000, immutable.
Keep an external image URL
This is quick initially, but article quality then depends on another site's retention policy, hotlink controls, terms, and latency. Keep source attribution links, but copy images you are allowed to publish into storage you control.
Fail before storing the object
An upload endpoint should verify administrator authentication first. It should then check Content-Length, actual byte size, and the allowed image formats. SVG can contain scripts, so a general photo-and-diagram uploader is safer when restricted to the formats it actually needs, such as JPEG, PNG, WebP, and GIF.
A date plus a unique ID makes stored objects easier to trace:
text
thumbnails/2026-07-16/<uuid>.webp
Never overwriting a key avoids the classic “the upload succeeded but the old image remains” problem caused by browser and CDN caches. A replacement should create a new object and update the article URL. Unreferenced objects can be removed later by a maintenance job after reference checks.
Preserve display intent in Markdown
Store a standard Markdown image line:
markdown

Alt text should describe the information conveyed by the figure, not restate the filename. “Architecture delivering an R2 image to an article” remains useful to a reader who cannot see the image; “image-01” does not.
When using a custom Markdown parser, document its supported syntax. If it recognizes image syntax only when it occupies a full line, place blank lines around every image and never append prose to the same line. A mismatch between authoring syntax and rendering syntax can leave raw Markdown visible on the public page.
Fix dimensions and allowed sources in `next/image`
Supplying width and height to Image establishes an aspect ratio before the asset arrives, reducing layout shift. For consistent 16:9 article diagrams, values such as width={1200} and height={675} describe the ratio, while CSS controls responsive width and object-fit.
When optimizing an external host, keep Next.js images.remotePatterns narrow. Limit protocol, hostname, and pathname to the exact source you operate. A same-origin Worker fetch route keeps public image URLs under your own domain and avoids continuously expanding the remote host allowlist.
Verify publication in six stages
A successful article API response does not prove that image delivery works. Between the uploader and the reader are the R2 binding, fetch route, Markdown parser, next/image, and browser.
Use this order:
1. Confirm upload success and a non-empty returned URL.
2. GET the image URL directly and verify HTTP 200 plus the expected `Content-Type`.
3. Insert alt-bearing Markdown into the article.
4. Check ratio and spacing in the draft preview.
5. Open the published article at desktop and mobile widths.
6. Check DevTools for 404, CSP, and image optimization errors.
If a stage fails, do not promote the article to published state, or fall back to a layout where the text remains readable. An image failure should not cascade into a page-level 500 response.
Cache for a long time by changing the URL
The simplest fast-image strategy is to avoid overwrites and change the URL whenever content changes. That makes a long max-age plus immutable safe.
Overwriting a fixed URL such as cover.png requires cache purges or a short TTL. It looks simpler in code, but production operations become harder because CDN, browser, and social Open Graph caches all need to refresh.
Common failure patterns
• Saving base64 image data inside the article JSON.
• Trusting only the client-supplied Content-Type.
• Writing an image URL into the article before object storage succeeds.
• Overwriting an existing key while serving it with an immutable cache policy.
• Generating empty alt text at scale.
• Trusting an admin success toast without directly fetching the image URL.
• Allowing every external image hostname through a broad wildcard.
Image delivery is a small data pipeline rather than a decorative UI component. Design storage, URLs, caching, Markdown, and verification as one system, and the CMS remains reliable as its image library grows.