Serve docs images from R2 instead of committing them to git - #134
Merged
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
cocos-docs | 34ef864 | Commit Preview URL Branch Preview URL |
Aug 07 2026, 09:11 AM |
ianmuchyri
force-pushed
the
feat/r2-image-cdn
branch
2 times, most recently
from
August 6, 2026 15:38
b10dd03 to
fae3cbc
Compare
Images under public/img/ were committed straight into the repo and bundled
into the Next.js static export at build time (markdown image syntax gets
compiled by fumadocs' remark-image plugin into a static import of the local
file). That meant every image add/update was a binary diff in git history,
and the build required the files to exist locally.
This moves image storage to the shared Cloudflare R2 bucket
("websites-images", object keys under the "cocos-docs" prefix so they don't
collide with other properties in the same bucket), fronted by a Cloudflare
Worker script that proxies /docs/cocos-ai/img/... requests to R2. The
site's build is a full static export (output: "export") deployed as
Cloudflare Workers static assets with no server runtime, so there's no
Next.js route handler that could do this proxying the way a normal SSR
deployment would - the Worker sits in front of the static asset handler
instead, only running for requests that don't match a file under out/
(which, now that images aren't built into out/, is every /img/ request).
The first pass of this migration converted markdown image syntax
(![]()) to raw <img>/<Image> JSX to stop the build from statically
importing local files, on the theory that a lowercase `img:` override in
mdx-components.tsx wasn't actually reachable from markdown-syntax images.
That's more ceremony than this change should require: authors should
keep writing plain markdown image syntax with the same `/img/...` paths
as before. Markdown-syntax images (``) always compile through
MDX's `_components.img`, confirmed by compiling sample MDX through
@mdx-js/mdx directly - the `img:` key just needs to actually be wired up
to render them, which it now is. Disabling fumadocs-mdx's remarkImage
plugin (source.config.ts) is what actually stops the build-time static
import; converting authoring syntax was never necessary for that.
Changes:
- wrangler.jsonc: add "main" pointing at the new Worker script, an
explicit "binding" name for the assets handler, and the IMAGES_BUCKET R2
binding (bucket_name "websites-images")
- worker/index.ts, worker/r2-proxy.ts: Worker script that proxies
/docs/cocos-ai/img/... to R2 and otherwise falls through to the static
asset handler; Env/R2Bucket/Fetcher types defined locally rather than
relying on the gitignored, wrangler-generated worker-configuration.d.ts
(pnpm run build never regenerates it, only the separate types:check
script does)
- source.config.ts: disable remarkImageOptions so markdown images aren't
statically imported at build time (the file no longer exists on disk)
- content/docs/**/*.mdx: reverted to the original plain markdown image
syntax (``)
- mdx-components.tsx: register a plain, zoomable `<img>` (ImageZoom,
no next/image, no width/height) under the lowercase `img` key only;
removed the capitalized `Image` component and next/image dependency
from the first pass. ImageZoom is given `src`/`alt` directly (not just
via `children`) since its zoomed-in view reads the image from that prop
- public/img/: remove all 16 image files (now served from R2)
- scripts/publish-image.mjs, scripts/README.md,
scripts/.env.publish-image.example: maintainer-only upload+purge tool,
modeled on the same tool in the absmach-website repo, adapted for this
repo's domain and base path
- package.json: add "publish-image" script; "types:check" runs
"wrangler types" first so the Worker's Env type is available to tsc
- .gitignore: ignore worker-configuration.d.ts and the maintainer's
scripts/.env.publish-image credentials file
- README.md: document the new worker/ directory and cross-link
scripts/README.md
Verified in the static export output (out/docs/cocos-ai/): every doc image
renders as a plain <img src="/docs/cocos-ai/img/...">, and ImageZoom
receives the same resolved src/alt as the thumbnail.
The production domain (www.ultraviolet.rs) is known from next.config.mjs
and README.md, but the Cloudflare zone ID isn't available from this repo,
so CLOUDFLARE_ZONE_ID in scripts/.env.publish-image.example is left as an
explicit TODO placeholder rather than guessed.
ianmuchyri
force-pushed
the
feat/r2-image-cdn
branch
from
August 6, 2026 16:14
fae3cbc to
5e523fd
Compare
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.
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
public/img/bloated PRs and repo history. They're now stored in the sharedwebsites-imagesR2 bucket (prefixcocos-docs) and served through a hand-written Cloudflare Worker (worker/index.ts) added asmainalongside the static-assets binding — this site is a fully static Next.js export with no server runtime.img:component override inmdx-components.tsx(meant to base-path-prefix image URLs) never fired for the doc images in this repo — traced this into MDX/JSX compilation semantics: lowercase JSX tags (<img>) always compile to hardcoded intrinsic elements that bypass thecomponentsoverride entirely; only capitalized tags (<Image>) route through it. Confirmed empirically by compiling sample MDX and inspecting the generated output. Fixed by registering the override under a capitalizedImagekey instead and converting the 16 doc-image references to use it — not a workaround, the actual mechanism now works.next/image'sImagecomponent (unoptimized: true) with explicitwidth/height, reusing dimensions already hand-extracted from git history (with one gap filled in:srtm.pngwas missing a height in the original markup, recovered from the source file itself).scripts/publish-image.mjs(maintainer-only, seescripts/README.md) to upload an image and purge the edge cache for it.public/img/(16 files, all referenced from MDX) from git. All 16 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 lint:md,pnpm run buildall pass locally withpublic/img/genuinely absent from diskdata-nimgpresent (genuine next/image output), no unprefixed/img/...remaining--remote) R2 bucket, spot-checked byte-identical against source🤖 Generated with Claude Code