Skip to content

docs: serve doc images from R2 instead of committing them to git - #57

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

docs: serve doc images from R2 instead of committing them to git#57
dborovcanin merged 3 commits into
mainfrom
feat/r2-image-cdn

Conversation

@ianmuchyri

Copy link
Copy Markdown
Contributor

Summary

Scope: docs/ only (this repo's Next.js docs site) — nothing outside docs/ is touched.

  • Doc images committed into docs/public/img/ bloated PRs and repo history. They're now stored in the shared websites-images R2 bucket (prefix atom-docs) and served through a hand-written Cloudflare Worker (docs/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 a Next.js route handler / next-on-pages / OpenNext binding wasn't an option.
  • Disabled fumadocs' remarkImage (source.config.ts: remarkImageOptions: false) so MDX images no longer need to exist locally at build time.
  • Added docs/components/doc-image.tsx, rendering next/image's Image component (unoptimized: true) with explicit width/height from docs/lib/image-dimensions.json (generated once from the original files, then kept in sync by the publish script going forward). Verified via wrangler dev (real workerd runtime, not just a build) that pages load correctly and the R2-proxy route responds as expected.
  • Added docs/scripts/publish-image.mjs (maintainer-only, see docs/scripts/README.md) to upload an image and purge the edge cache for it.
  • Removed docs/public/img/ (88 files, all actually referenced from content/docs/**/*.mdx) from git. All 88 have been uploaded to the real R2 bucket and spot-checked byte-for-byte against the originals.

Test plan

  • pnpm run build, pnpm run types:check (including the Worker), lint all pass locally with docs/public/img/ genuinely absent from disk
  • wrangler dev smoke test: normal pages fall through to static assets, an R2-seeded test object serves with the correct cache-control headers at the exact URL the built HTML references, missing images return a clean 404
  • All 88 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
atom-docs 1706b82 Commit Preview URL

Branch Preview URL
Aug 07 2026, 10:51 AM

Images committed into docs/public/img/ bloated PRs and repo history.
They're now stored in the shared "websites-images" R2 bucket (prefix
"atom-docs") and served through a hand-written Cloudflare Worker
(docs/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.
- docs/components/doc-image.tsx 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 and the manifest-sync logic that
  used to live in scripts/publish-image.mjs -- simplified back to
  just upload + purge.

All 88 images have already been uploaded to the real R2 bucket and
spot-checked byte-for-byte against the originals.
env.IMAGES_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
scripts/publish-image.mjs 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.

This repo already depends on @cloudflare/workers-types (unlike the
sister sites), so Cache/CacheStorage/ExecutionContext are used via
their ambient global types instead of local structural interfaces.

Same fix as absmach/website#178, applied here since this repo's
worker/index.ts uses the identical binding-without-caching pattern.
pnpm audit --prod --audit-level high was failing in CI on js-yaml
4.3.0, pulled in transitively via fumadocs-mdx (.>fumadocs-mdx>js-yaml).
fumadocs-mdx's own declared range ("^4.1.0") already permits the
patched 4.3.1, so this is pinned via a pnpm.overrides entry rather
than waiting on fumadocs-mdx to bump its lockfile-resolved version
itself.

Also pins sharp and postcss to the versions they were already
resolving to before this change (^0.35.3 and ^8.5.18, both already
past their own advisories' patched thresholds) -- introducing the
js-yaml override alone caused pnpm to re-resolve the whole dependency
graph and land on older, vulnerable versions of both (sharp
0.35.3->0.34.5, plus a stray duplicate postcss@8.4.31 no longer
deduped with the top-level 8.5.18), surfacing 2 more high-severity
findings that don't exist in a plain install of main. Pinning all
three keeps the lockfile at exactly the versions already known safe,
with no other dependency churn (confirmed via lockfile diff).

Pre-existing, unrelated to the R2 image-caching work on this branch --
confirmed the same js-yaml advisory already fails audit on a clean
install of main.
@dborovcanin
dborovcanin merged commit 5e48932 into main Aug 7, 2026
6 checks passed
@dborovcanin
dborovcanin deleted the feat/r2-image-cdn branch August 7, 2026 11:07
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