cloudflare / cloudflare/vinext
Generated metadata images are not statically optimized by default
- Dominant language
- TypeScript
- Stars
- 8.8k
- Forks
- 406
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 120
Description
## Summary
In `vinext@1.0.0-beta.6`, a generated metadata image is considered a prerender candidate only when its default export has vinext's `USE_CACHE_FUNCTION_SYMBOL`. In practice, that marker is added by the callable function-level `"use cache"` transform.
This makes `"use cache"` appear necessary to prerender a static `opengraph-image.tsx`, even though generated metadata images in Next.js are statically optimized by default when they do not use dynamic inputs.
`"use cache"` is not a safe workaround for a route returning `ImageResponse`: it puts the `Response` through the callable React Flight cache, where it is not a valid return value.
## Environment
- vinext: 1.0.0-beta.6
- `@vitejs/plugin-rsc`: 0.5.34
- React / React DOM / `react-server-dom-webpack`: 19.2.7
- Vite: 8.1.3
- Node.js: 22.13.1
- pnpm: 11.1.1
Next.js reference used for behavior and source comparison:
- Next.js: 16.3.0-canary.105
- commit: `ccd47cfe53c7fef5e6b7ad472a3a020605a23608`
## Reproduction
https://github.com/MaxtuneLee/vinext-image-response-use-cache-repro
Create a static generated metadata image without `"use cache"`:
```tsx
// app/opengraph-image.tsx
import { ImageResponse } from "next/og";
export const size = { width: 1200, height: 630 };
export default function OpenGraphImage() {
return new ImageResponse(
static metadata image
size,
);
}
```
Run:
```sh
pnpm vinext build --prerender-all
```
Inspect `dist/server/vinext-prerender.json` and `dist/server/prerendered-routes/`.
## Actual behavior
The generated metadata route is excluded from metadata prerender candidates unless its default export has `Symbol.for("vinext.useCacheFunction")`. A normal static `opengraph-image.tsx` has no such marker because it does not use the callable `"use cache"` transform.
As a result, a route that is otherwise static does not get the metadata response prerender artifact solely because it has no callable-cache wrapper.
Adding `"use cache"` makes it eligible, but changes the execution path: the `ImageResponse` is then passed to the callable React Flight serializer. That is a separate incorrect behavior, not a valid way to opt into metadata response caching.
## Expected behavior (Next.js behavior)
Next.js statically optimizes generated `opengraph-image.tsx` routes by default. A static-eligible route does not need `"use cache"`; Next.js creates it at build time and serves it from the route response cache.
It becomes dynamic only when its code uses request-time APIs, uncached data, or explicit dynamic route configuration. This is documented in the Next.js [`opengraph-image` file-convention documentation](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image).
vinext should similarly:
- consider a generated metadata image as a metadata/App Route prerender candidate without requiring a callable-cache marker;
- use route dynamic-usage and cacheability state to decide whether it can be persisted;
- store the returned HTTP `Response` body, status, and headers as the metadata response artifact.
## Possible root cause
`packages/vinext/src/server/metadata-route-response.ts` uses `Symbol.for("vinext.useCacheFunction")` on the metadata default export as the eligibility test in `getPrerenderableMetadataRoutePaths()`.
The symbol indicates that the function went through `packages/vinext/src/plugins/use-cache-callable.ts`. It is therefore a proxy for a particular implementation detail, not evidence that a metadata route is static or response-cacheable.
This conflates two independent concerns:
1. whether the metadata route can be rendered at build time and its HTTP response persisted;
2. whether the function's JavaScript return value is eligible for callable React Flight caching.
An `ImageResponse` is valid for the first concern and invalid for the second.
## Suggested fix
1. Remove `USE_CACHE_FUNCTION_SYMBOL` as a prerequisite for metadata route prerender eligibility and metadata response caching.
2. Send generated metadata routes through the normal metadata/App Route prerender path, then use dynamic-usage and cacheability signals to determine whether to write an artifact.
3. Keep `buildAppRouteCacheValue(response)` as the response-cache representation for generated images.
4. Add an integration test for an `opengraph-image.tsx` with no `"use cache"` that asserts a 200 response artifact is written during `vinext build --prerender-all`.
5. Add a dynamic metadata-image test to confirm request-time APIs or uncached data still prevent static prerendering.
Contributor guide
Research direction
Start with packages/vinext/src/server/metadata-route-response.ts and packages/vinext/src/plugins/use-cache-callable.ts, then run pnpm vinext build --prerender-all on the reproduction and inspect the prerender manifest and output. Done means a static opengraph-image.tsx without "use cache" produces a 200 response artifact using buildAppRouteCacheValue(response), while a dynamic metadata image is not statically prerendered.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- api, backend, build-system
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100