Skip to content

Instantly share code, notes, and snippets.

@julrich
Created May 22, 2026 10:31
Show Gist options
  • Select an option

  • Save julrich/ba656574f0fca01de25d5778c56639b3 to your computer and use it in GitHub Desktop.

Select an option

Save julrich/ba656574f0fca01de25d5778c56639b3 to your computer and use it in GitHub Desktop.

Here's how I'd approach it. Quick summary of what's in place today so the plan is grounded:

  • The Image from @unpic/react/nextjs is wired up centrally in ComponentProviders.tsx — i.e. there's one funnel for every image render.
  • Storyblok URLs flow through as https://a.storyblok.com/f/<space>/<WxH>/<hash>/<name> (visible in types/components-presets.json). The <hash> is a perfect dedup key.
  • The project is already targeted at Cloudflare (wrangler.jsonc, open-next.config.ts).

Recommended shape

1. Don't do the upload inside getStaticProps. Do the rewrite there, but the upload in a dedicated pre-build step.

Reasons:

  • getStaticProps runs per page, in parallel, in worker processes — sharing an in-memory dedup map across them is awkward, and you end up needing an on-disk lock/manifest anyway. Once you have that manifest, you don't need to do anything async during the page build.
  • A single pre-build script gives you one place to handle retries, rate limits, incremental uploads, and CI caching of the manifest.

So:

scripts/mirror-images.ts        # crawls Storyblok, uploads new assets, writes manifest
cms/asset-manifest.json         # storyblokHash -> { cfImageId, width, height, isSvg }
helpers/rewriteAssetUrls.ts     # walks story content, swaps filenames using manifest

Wire mirror-images as "prebuild" in package.json, then have the central Storyblok-fetch helper apply rewriteAssetUrls when process.env.NODE_ENV === 'production' && !preview. Dev, preview, and the Storyblok visual editor stay 100% on a.storyblok.com — they never see the manifest path.

2. Use the Cloudflare Images product (not just R2 + /cdn-cgi/image/).

Both options reduce Storyblok egress, but Cloudflare Images is what the Unpic cloudflare_images provider targets directly, has built-in dedup-friendly IDs, and removes the need to put a Worker / zone in front of R2 just to get resizing. You'll want to enable Flexible Variants in the Images dashboard so Unpic can request arbitrary widths; otherwise you have to pre-define named variants per use case.

Pipeline per asset:

  • Hash key = the <hash> segment from the Storyblok URL (already content-addressed by Storyblok).
  • If manifest[hash] exists → skip.
  • Otherwise: fetch(storyblokUrl)POST to https://api.cloudflare.com/client/v4/accounts/<acct>/images/v1 with the bytes → store the returned id, plus the original WxH parsed from the Storyblok path, in the manifest.

3. Let Unpic switch providers automatically.

Once you rewrite the URL from a.storyblok.com/... to https://imagedelivery.net/<account-hash>/<id>/public, @unpic/react detects Cloudflare Images on its own. You don't have to touch ComponentProviders.tsx or pass a provider prop — the URL is the switch. That's why doing the rewrite at the data layer (where stories enter the app) is enough.

4. Keep dimensions intact.

Your components rely on the WxH chunk in the Storyblok URL for aspect-ratio handling. Store width/height in the manifest at upload time and either:

  • Synthesize a URL that still carries those dims for your own parsing, or
  • Have rewriteAssetUrls also write meta.width/meta.height onto the asset object so the consumers stop relying on regexing the URL.

The second is cleaner long-term.

5. Caveats worth flagging up-front

  • SVGs: Cloudflare Images doesn't transform SVG (avatars, dividers, logos in your presets are SVG). Either pass them through unchanged (leave on Storyblok or upload to R2 and serve raw, no /cdn-cgi/image/), or mark them in the manifest with isSvg: true and have rewriteAssetUrls skip them.
  • Flexible Variants must be enabled on the Images account, otherwise Unpic's per-width srcset URLs will 404.
  • Manifest in CI: persist cms/asset-manifest.json across builds (commit it, or cache it in CI) so each build only uploads net-new hashes. Without that, every clean build re-uploads everything — slow and burns the Images "uploads" quota.
  • Preview-only content: if editors add an asset and hit "Publish", a production deploy will reupload it on next build. Until then, production keeps the previous version. Acceptable, but worth documenting.
  • Existing presets/types: paths in types/components-presets.json are baked into type fixtures, not live content — leave them on Storyblok. Only rewrite live story content.
  • Pricing: Cloudflare Images is $5/100k stored + $1/100k delivered (flat). For a kit serving many landing pages this is cheap, but worth confirming against current Storyblok bandwidth before committing.

6. Env

CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_IMAGES_TOKEN, CLOUDFLARE_IMAGES_HASH (the public account hash that appears in imagedelivery.net/<hash>/...). Only the public hash should ever be referenced at runtime; the token lives in CI only.


If you want, I can go ahead and scaffold scripts/mirror-images.ts, the manifest type, and the rewrite helper, wired up against your existing Storyblok client — say the word and I'll implement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment