Skip to content

Serve images from Cloudflare R2 instead of the repo - #71

Merged
dborovcanin merged 2 commits into
mainfrom
feat/r2-image-cdn
Aug 7, 2026
Merged

Serve images from Cloudflare R2 instead of the repo#71
dborovcanin merged 2 commits into
mainfrom
feat/r2-image-cdn

Conversation

@ianmuchyri

Copy link
Copy Markdown
Contributor

Summary

  • Images committed into public/img/ bloated PRs and repo history. They're now stored in the shared websites-images R2 bucket (prefix cube-docs) and served through a hand-written Cloudflare Worker (worker/index.ts) added as main alongside the static-assets binding — this site is a fully static Next.js export with no server runtime.
  • Disabled fumadocs' remarkImage (source.config.ts), which previously bundled referenced images into the webpack build at compile time.
  • Images now render via next/image's Image component (unoptimized: true) with explicit width/height from a generated lib/image-dimensions.json manifest, replacing the prior plain-<img> rendering. components/BrandLogo.tsx's 2 fixed-size SVG logos were intentionally left out of this migration (out of scope, same boundary drawn on sibling repos) — they already used next/image correctly and aren't referenced from any MDX content.
  • Added scripts/publish-image.mjs (maintainer-only, see scripts/README.md) to upload an image and purge the edge cache for it.
  • Removed public/img/ (58 content images) from git. All 58 have been uploaded to the real R2 bucket and spot-checked byte-for-byte against the originals.

Test plan

  • pnpm run lint, pnpm run types:check, pnpm run build all pass locally with images genuinely absent from disk
  • Verified in the built HTML: data-nimg present on every doc image (genuine next/image output), correct dimensions, no srcset (confirms unoptimized)
  • All 58 images uploaded to the real (--remote) R2 bucket, spot-checked byte-identical against source
  • Confirm images render correctly on this PR's preview deployment before merging

🤖 Generated with Claude Code

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 6, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
cube-docs 18462c0 Commit Preview URL

Branch Preview URL
Aug 07 2026, 09:36 AM

Images committed into public/img/ bloated PRs and repo history.
They're now stored in the shared "websites-images" R2 bucket (prefix
"cube-docs") and served through a hand-written Cloudflare Worker
(worker/index.ts) added as `main` alongside the static-assets
binding -- this site is a fully static Next.js export with no server
runtime, so neither @cloudflare/next-on-pages nor
@opennextjs/cloudflare applies.

Authoring is unchanged: MDX content already referenced doc images by
their final /img/... path (no relative-path convention existed here
to begin with), so nothing about writing ![alt](/img/foo.png) needed
to change.

- source.config.ts disables fumadocs' remarkImage plugin, which
  otherwise needs the file on local disk at build time.
- mdx-components.tsx's img: override renders the result as a plain,
  zoomable <img> (fumadocs-ui's ImageZoom wrapping a plain element,
  not next/image) -- no width or height required, so there's nothing
  to keep in sync when images change. ImageZoom's src/alt are passed
  explicitly (not just to the inner <img>), since its zoomed-in view
  reads the image from those props directly, not from `children`.
- Dropped lib/image-dimensions.json entirely.

All 58 content images have already been uploaded to the real R2
bucket and spot-checked byte-for-byte against the originals.
bucket.get() is an R2 binding call, not an HTTP subrequest. Workers
run before Cloudflare's cache in the request pipeline, so a Response
the Worker constructs and returns is never automatically written into
the edge cache, no matter what Cache-Control header is set on it --
that only happens via explicit Cache API use, or a zone Cache Rule
intercepting it. Neither was happening here, so despite
s-maxage=31536000 being set, every request (every visitor, every edge
location) was a live R2 read.

Fixed by writing responses into the Workers Cache API (caches.default)
after the first R2 read, keyed by the request's own URL unmodified (so
it stays purgeable by the existing purge-by-URL call in the
publish-image script on every upload). This also adds Range/206
support as a side effect: cache.match() automatically serves 206
Partial Content for a Range request against a cached 200 response.

Bumped browser max-age from 300s to 3600s while leaving s-maxage at a
year -- purge-on-publish already invalidates the edge instantly on
every upload, so there's no freshness benefit to a short edge TTL.

Same fix as absmach/website#178, applied here since this repo's
worker/r2-proxy.ts uses the identical binding-without-caching pattern.
@dborovcanin
dborovcanin merged commit 8b1458c into main Aug 7, 2026
2 checks passed
@dborovcanin
dborovcanin deleted the feat/r2-image-cdn branch August 7, 2026 10:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants