Serve docs diagrams from R2 instead of committing them to git - #45
Merged
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
propeller-docs | 4934250 | Commit Preview URL Branch Preview URL |
Aug 07 2026, 08:59 AM |
ianmuchyri
force-pushed
the
feat/r2-image-cdn
branch
from
August 6, 2026 14:53
c781e81 to
e7df64a
Compare
Diagrams committed into content/docs/images/ bloated PRs and repo history. They're now stored in the shared "websites-images" R2 bucket (prefix "propeller-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: markdown image syntax stays exactly as it was, relative paths and all -- , . This works via: - source.config.ts disables fumadocs' remarkImage plugin, which otherwise needs the file on local disk at build time. - src/lib/remark-doc-images.ts, a small remark plugin, resolves each image's path relative to its source .mdx file's own location into the literal /docs/propeller/img/... URL the Worker serves -- pure path math, no image bytes needed, no manifest. - src/mdx-components.tsx's `img:` override renders the resolved 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. External image references (e.g. an asciinema embed) pass through untouched. ImageZoom is given `src`/`alt` directly, not just via `children`: its zoomed-in view reads the image from that prop, so omitting it rendered a blank zoomed-in image even though the inline thumbnail looked correct. Passing `src` there required explicitly typing `props` as `ComponentPropsWithoutRef<"img">` with a `typeof props.src !== "string"` narrow beforehand -- left implicit, `props.src` picks up `Blob` from React's experimental `img` src types (which Next 16.2's type surface references), which ImageZoom's own `src?: string | StaticImport` prop type rejects. scripts/publish-image.mjs (maintainer-only, see scripts/README.md) uploads an image and purges the edge cache for it -- simplified back to just those two steps, no manifest bookkeeping. All 40 actually- referenced diagrams (of 45 total; 5 were orphaned and dropped) 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 16:26
e7df64a to
49b3fc3
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 (prefixpropeller-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 (output: "export") with no server runtime, so neither@cloudflare/next-on-pagesnor@opennextjs/cloudflareapplies; Cloudflare's default routing serves matching static files directly and only invokes the Worker on an asset miss.remarkImage(source.config.ts), which previously bundled every locally-referenced diagram into the webpack build at compile time (content-hashed_next/static/media/...), requiring the file on disk at build time — incompatible with removing them from git.next/image'sImagecomponent (unoptimized: true, required for a static export with no image-optimization server) with explicitwidth/heightfrom a generatedsrc/lib/image-dimensions.jsonmanifest, instead of a plain<img>tag — real dimensions were pulled from the pre-removal git commit and measured directly (not guessed), since an earlier build without R2 credentials had briefly generated placeholder-sized SVGs that would have baked in wrong aspect ratios.scripts/publish-image.mjs(maintainer-only, seescripts/README.md) to upload a diagram and purge the edge cache for it.content/docs/images/(40 files, after dropping 5 that weren't actually referenced from any MDX) from git. All 40 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 check:assets,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