Serve docs images from R2 instead of committing them to git - #15
Open
ianmuchyri wants to merge 2 commits into
Open
Serve docs images from R2 instead of committing them to git#15ianmuchyri wants to merge 2 commits into
ianmuchyri wants to merge 2 commits into
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
hardware-docs | 2d903f6 | Commit Preview URL Branch Preview URL |
Aug 07 2026, 08:41 AM |
Content images under content/docs/images/ were committed as binary files and referenced from MDX with relative paths (../images/foo.png). fumadocs-mdx's static-export bundler resolved those at build time into content-hashed files, which meant the image bytes had to be physically present in the repo just to run `next build`. That's a growing repo and doesn't fit the shared image-hosting setup other Abstract Machines properties are moving to (a single R2 bucket, proxied same-origin so URLs don't change, with immediate cache purge on update). Since this site is a pure Next.js static export with no server runtime in production (deployed as Cloudflare static assets, no next-on-pages or opennextjs/cloudflare), there's no Next.js request handler to reach an R2 binding from. Instead, wrangler.jsonc now has a `main` Worker script that falls back from the ASSETS binding (only when no static file matches, since run_worker_first defaults to false) to reading `/docs/hardware/img/*` straight out of the shared `websites-images` bucket, under the `hardware-docs` key prefix. The first pass of this migration also changed MDX authoring syntax (relative paths -> absolute `/img/<file>`) and added a width/height JSON manifest (src/lib/image-dimensions.json) rendered through next/image, so every new image required a manual manifest entry or the build would throw. That's more ceremony than this change should require: authors should keep writing plain markdown image syntax with whatever relative path they always used, and nothing about next/image's optimizer is actually usable here anyway (this is a static export with no image-optimization server, `unoptimized` was already set). This commit reverts the MDX content and syntax to their original form and replaces DocImage/next/image with a plain, zoomable `<img>`: - content/docs/**/*.mdx: reverted to the original relative-path markdown image syntax (`../images/foo.png`) - src/lib/remark-doc-images.ts: new remark plugin that resolves each image's relative path against content/docs/images/ at compile time (pure path math, no image bytes needed) into the literal `/docs/hardware/img/...` URL the Worker serves — replaces fumadocs-mdx's remarkImage, which needs the file on disk - source.config.ts: disable remarkImageOptions, wire in remarkDocImages - src/mdx-components.tsx: render the `img` MDX node as a plain `<img>` (no next/image, no width/height) wrapped in ImageZoom for click-to-zoom, passing `src`/`alt` to ImageZoom itself as well as the inner `<img>` — ImageZoom's zoomed-in view reads its image from that prop directly, not from `children` - src/components/doc-image.tsx, src/lib/image-dimensions.json: removed, no longer needed - worker/index.ts, wrangler.jsonc: unchanged from the first pass - scripts/publish-image.mjs, scripts/README.md, scripts/.env.publish-image.example: maintainer-only upload+purge CLI and its docs, unchanged from the first pass
ianmuchyri
force-pushed
the
feat/r2-image-cdn
branch
from
August 6, 2026 16:09
8110a66 to
c23a5c4
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. Same fix as absmach/website#178, applied here since this repo's worker/index.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
content/docs/images/bloated PRs and repo history. They're now stored in the sharedwebsites-imagesR2 bucket (prefixhardware-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, so a Next.js route handler / next-on-pages / OpenNext binding wasn't an option; Cloudflare serves matching static files directly and only invokes the Worker on an asset miss.remarkImage(source.config.ts), which previously bundled referenced images into the webpack build at compile time, requiring the file on disk at build time.next/image'sImagecomponent (unoptimized: true) with explicitwidth/heightfromsrc/lib/image-dimensions.json, instead of a plain<img>tag. Along the way, found that the prior in-place override already routed throughnext/image'sImageindirectly but never supplied dimensions — meaning it silently shipped zero real CLS protection despite looking optimized in the DOM. Fixed by feeding it real measured dimensions instead, while deliberately preserving the existing click-to-zoom (ImageZoom) UX rather than ripping it out.scripts/publish-image.mjs(maintainer-only, seescripts/README.md) to upload an image and purge the edge cache for it.content/docs/images/from git — of 53 files present, only 27 were actually referenced from MDX (confirmed via grep); the other 26 were orphaned and dropped rather than migrated. All 27 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 buildall pass locally withcontent/docs/images/genuinely absent from disknpx wrangler deploy --dry-runvalidates both theIMAGES_BUCKETandASSETSbindings--remote) R2 bucket, spot-checked byte-identical against source🤖 Generated with Claude Code