docs: serve doc images from R2 instead of committing them to git - #57
Merged
Conversation
Deploying with
|
| 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  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.
ianmuchyri
force-pushed
the
feat/r2-image-cdn
branch
from
August 6, 2026 15:54
7230466 to
75a4c1a
Compare
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.
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Scope:
docs/only (this repo's Next.js docs site) — nothing outsidedocs/is touched.docs/public/img/bloated PRs and repo history. They're now stored in the sharedwebsites-imagesR2 bucket (prefixatom-docs) and served through a hand-written Cloudflare Worker (docs/worker/index.ts) added asmainalongside 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.remarkImage(source.config.ts:remarkImageOptions: false) so MDX images no longer need to exist locally at build time.docs/components/doc-image.tsx, renderingnext/image'sImagecomponent (unoptimized: true) with explicitwidth/heightfromdocs/lib/image-dimensions.json(generated once from the original files, then kept in sync by the publish script going forward). Verified viawrangler dev(realworkerdruntime, not just a build) that pages load correctly and the R2-proxy route responds as expected.docs/scripts/publish-image.mjs(maintainer-only, seedocs/scripts/README.md) to upload an image and purge the edge cache for it.docs/public/img/(88 files, all actually referenced fromcontent/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 withdocs/public/img/genuinely absent from diskwrangler devsmoke 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--remote) R2 bucket, spot-checked byte-identical against source🤖 Generated with Claude Code