cloudflare / cloudflare/vinext

Generated metadata images are not statically optimized by default

Open
#2,950 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.