[PR #356] [MERGED] feat(og): add @next/og / @vercel/og support with Cloudflare Workers compatibility #508

Closed
opened 2026-05-06 13:08:27 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/356
Author: @james-elicx
Created: 3/8/2026
Status: Merged
Merged: 3/8/2026
Merged by: @james-elicx

Base: mainHead: opencode/proud-pixel


📝 Commits (5)

  • 6ea29ac feat(og): add @next/og support with Cloudflare Workers compatibility
  • 4031c1c refactor(og): generalize fetch+import.meta.url asset inlining, drop WASM lazy-init patch
  • 028476a fix(og): inline readFileSync assets from @vercel/og node build
  • 8c5c9f0 refactor(og): address code review feedback
  • db47446 refactor(og): use replaceAll instead of split().join()

📊 Changes

6 files changed (+407 additions, -16 deletions)

View changed files

examples/app-router-cloudflare/app/api/og/route.tsx (+47 -0)
📝 packages/vinext/src/index.ts (+174 -9)
📝 packages/vinext/src/shims/og.tsx (+6 -6)
📝 playwright.config.ts (+7 -1)
tests/e2e/og-image.spec.ts (+126 -0)
tests/fixtures/app-basic/app/api/og/route.tsx (+47 -0)

📄 Description

Summary

Adds first-class @next/og (OG image generation) support to vinext, including full compatibility with Cloudflare Workers in both dev (cloudflare-dev) and production (cloudflare-workers) modes.

Problem

@vercel/og's edge build (dist/index.edge.js) loads its fallback font at module init time using:

fetch(new URL("./noto-sans-v27-latin-regular.ttf", import.meta.url)).then(res => res.arrayBuffer())

In Cloudflare Workers, import.meta.url is the string "worker" — not a real URL — so new URL(...) throws TypeError: Invalid URL string and the Worker fails to start before serving any request.

A secondary issue was that Vite's esbuild pre-bundler would cache @vercel/og before our transform hook ran, causing the uncached module to be served into a Node.js context where WebAssembly.instantiate() is blocked.

Fix

vinext:og-inline-fetch-assets transform plugin (enforce: "pre"):

Matches any fetch(new URL("./asset", import.meta.url)).then(res => res.arrayBuffer()) expression — regardless of filename — and replaces it with an inline base64 IIFE that reads the file from disk at Vite transform time and decodes it synchronously. This eliminates the runtime fetch entirely and works in all environments.

This is intentionally general: it handles any font version bump in @vercel/og, additional fonts, and any other library using the same pattern.

@vercel/og added to optimizeDeps.exclude (global, rsc, and ssr environments): prevents Vite's esbuild pre-bundler from caching the module before the transform hook runs. With this in place, the module always flows through our transform and executes inside workerd where WebAssembly.instantiate() is fully supported — no WASM workarounds needed.

vinext:og-assets build plugin: copies resvg.wasm to the RSC output directory for production builds (the font is now inlined, so only the WASM needs to be present as a file).

Tests

  • New OG route (app/api/og/route.tsx) in both examples/app-router-cloudflare and tests/fixtures/app-basic, accepting a ?title= query param
  • Shared Playwright e2e spec (tests/e2e/og-image.spec.ts) with 6 tests:
    • Returns 200 with content-type: image/png
    • Valid PNG signature + correct 1200×630 dimensions (read from IHDR chunk)
    • No-param default title works
    • ?title=Hello custom title works
    • Different params produce different PNG bytes
    • Same param produces identical PNG bytes (deterministic)
  • 18 tests passing across app-router, cloudflare-workers, and cloudflare-dev

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/356 **Author:** [@james-elicx](https://github.com/james-elicx) **Created:** 3/8/2026 **Status:** ✅ Merged **Merged:** 3/8/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `opencode/proud-pixel` --- ### 📝 Commits (5) - [`6ea29ac`](https://github.com/cloudflare/vinext/commit/6ea29ac1dcf01df18bf9062dae01eb8e17987501) feat(og): add @next/og support with Cloudflare Workers compatibility - [`4031c1c`](https://github.com/cloudflare/vinext/commit/4031c1caad95467db007c1a5ccf2467e717c5d73) refactor(og): generalize fetch+import.meta.url asset inlining, drop WASM lazy-init patch - [`028476a`](https://github.com/cloudflare/vinext/commit/028476a8931fd579e4af36dc9b35edb713a2001a) fix(og): inline readFileSync assets from @vercel/og node build - [`8c5c9f0`](https://github.com/cloudflare/vinext/commit/8c5c9f0b3c74a6916eac7ffdb0909ac234267734) refactor(og): address code review feedback - [`db47446`](https://github.com/cloudflare/vinext/commit/db47446a709a0c7f16ec73b36619e8eccd6787a0) refactor(og): use replaceAll instead of split().join() ### 📊 Changes **6 files changed** (+407 additions, -16 deletions) <details> <summary>View changed files</summary> ➕ `examples/app-router-cloudflare/app/api/og/route.tsx` (+47 -0) 📝 `packages/vinext/src/index.ts` (+174 -9) 📝 `packages/vinext/src/shims/og.tsx` (+6 -6) 📝 `playwright.config.ts` (+7 -1) ➕ `tests/e2e/og-image.spec.ts` (+126 -0) ➕ `tests/fixtures/app-basic/app/api/og/route.tsx` (+47 -0) </details> ### 📄 Description ## Summary Adds first-class `@next/og` (OG image generation) support to vinext, including full compatibility with Cloudflare Workers in both dev (`cloudflare-dev`) and production (`cloudflare-workers`) modes. ### Problem `@vercel/og`'s edge build (`dist/index.edge.js`) loads its fallback font at module init time using: ```js fetch(new URL("./noto-sans-v27-latin-regular.ttf", import.meta.url)).then(res => res.arrayBuffer()) ``` In Cloudflare Workers, `import.meta.url` is the string `"worker"` — not a real URL — so `new URL(...)` throws `TypeError: Invalid URL string` and the Worker fails to start before serving any request. A secondary issue was that Vite's esbuild pre-bundler would cache `@vercel/og` before our transform hook ran, causing the uncached module to be served into a Node.js context where `WebAssembly.instantiate()` is blocked. ### Fix **`vinext:og-inline-fetch-assets` transform plugin** (`enforce: "pre"`): Matches any `fetch(new URL("./asset", import.meta.url)).then(res => res.arrayBuffer())` expression — regardless of filename — and replaces it with an inline base64 IIFE that reads the file from disk at Vite transform time and decodes it synchronously. This eliminates the runtime fetch entirely and works in all environments. This is intentionally general: it handles any font version bump in `@vercel/og`, additional fonts, and any other library using the same pattern. **`@vercel/og` added to `optimizeDeps.exclude`** (global, `rsc`, and `ssr` environments): prevents Vite's esbuild pre-bundler from caching the module before the transform hook runs. With this in place, the module always flows through our transform and executes inside workerd where `WebAssembly.instantiate()` is fully supported — no WASM workarounds needed. **`vinext:og-assets` build plugin**: copies `resvg.wasm` to the RSC output directory for production builds (the font is now inlined, so only the WASM needs to be present as a file). ### Tests - New OG route (`app/api/og/route.tsx`) in both `examples/app-router-cloudflare` and `tests/fixtures/app-basic`, accepting a `?title=` query param - Shared Playwright e2e spec (`tests/e2e/og-image.spec.ts`) with 6 tests: - Returns 200 with `content-type: image/png` - Valid PNG signature + correct 1200×630 dimensions (read from IHDR chunk) - No-param default title works - `?title=Hello` custom title works - Different params produce different PNG bytes - Same param produces identical PNG bytes (deterministic) - **18 tests passing** across `app-router`, `cloudflare-workers`, and `cloudflare-dev` --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:08:27 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#508
No description provided.