Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions apps/web/content/blog/vinext-1-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
title: "Vinext 1.2: middleware for public files, stricter caching, and broader ecosystem compatibility"
description: "Middleware runs for public/ files on Workers, cache keys that can't be shared between different requests, full-document Pages ISR regeneration, and next/dynamic parity."
date: "2026-10-14"
authors:
- name: James Anderson
url: https://github.com/james-elicx
tags:
- release
- Next.js
- Cloudflare Workers
- middleware
- caching
- next/dynamic
---

To upgrade:

```sh
npm install vinext@1.2.0 @vinext/cloudflare@1.1.1 @vitejs/plugin-rsc@^0.5.37
```

App Router projects now need `@vitejs/plugin-rsc` 0.5.37 or newer, and stop with an error on older versions. The reason is under [Server Actions](#server-actions). Projects created with `vinext init` have it as a dev dependency, so upgrade it alongside Vinext.

Most of this release is fixes. Many came from checking Vinext's behaviour against Next.js 16, from the nightly Next.js deploy test suite, and from running next-intl's own tests against Vinext.

## Middleware and public files on Workers

In Next.js, middleware runs for every path its `matcher` matches, including files in `public/`. On Workers, Static Assets serves those files before the Worker runs, so middleware never saw them.

`vinext init` now generates a `runWorkerFirst` setting that sends everything except the hashed `/_next/static` build output through the Worker:

```ts
assets: {
htmlHandling: "none",
notFoundHandling: "none",
runWorkerFirst: ["/*", "!/_next/static/*", "!/*/_next/static/*"],
},
```

Middleware then runs before `public/` files, and Vinext serves the file from the `ASSETS` binding with any request headers middleware changed. **Existing projects aren't changed.** If your middleware should cover public files, add these settings to your config. Keep both exclusions: `runWorkerFirst: true` also sends the build output to the Worker, which returns 404 for it. [Middleware and static files](/docs/deploying/cloudflare#middleware-and-static-files) has the details.

The image optimizer follows the same rules now. `/_next/image` requests go through middleware, and a local image source is fetched through middleware as an anonymous request, as Next.js does. A public image that middleware blocks can no longer be read through `/_next/image?url=…`. With a `basePath`, `next/image` also emits `${basePath}/_next/image`, which fixes 404s for optimized images on Workers apps without an image optimizer.

## Cache keys

Several fixes are for edge cases where two different requests could get the same cache key, so one caller's cached result could be served to another:

- `unstable_cache` keys include the callback source even when you pass `keyParts`, as in Next.js, and are now a SHA-256 digest of an unambiguous encoding. Tuples such as `["a:b", "c"]` and `["a", "b:c"]` no longer share an entry.
- An `unstable_cache` result is stored under the arguments its callback ran with, so changing a shared options object between calls no longer stores one user's data under another's key.
- A cached `fetch` copies its URL, headers, `cf` and body when it is called, then keys and sends that copy. Bodies of every type are keyed, and on Workers so is `cf.resolveOverride`. The [caching guide](/docs/guides/caching#how-cached-fetch-request-bodies-are-keyed) lists where this goes further than Next.js.
- Long ISR paths and other oversized keys are shortened with SHA-256 rather than a non-cryptographic hash.
- `x-api-key` counts as a credential, so responses to those requests are never stored in the shared Cloudflare caches.
- `revalidateTag()` in the default in-memory cache no longer forgets an invalidation after 10,000 other tags.

## Pages Router ISR

Stale Pages Router entries used to be regenerated by rendering only the page and splicing it into the cached HTML. `_document`, its `getInitialProps` and any CSS-in-JS styles stayed as they were on the first render. Regeneration now runs the full render, as Next.js does. Vinext also finds `<Main />` with a placeholder that request data can't forge, instead of searching for marker text.

In production, Next.js never calls `getStaticPaths()` for a request. Vinext has no required prerender step, so it still checks new paths at request time, but it no longer calls `getStaticPaths()` for cache hits, stale responses, draft mode, or when Response Store or Workers Cache regenerates an entry it already holds. Before this, a dynamic `getStaticProps` page ran its full path list on every request unless you used the static assets cache.

A `getStaticProps` page whose `_app` or `_document` sends the response itself, for example an auth redirect with `res.writeHead(302)`, is now still rendered and stored, as in Next.js.

## next/dynamic

[@Mauladen](https://github.com/Mauladen) and [@AhmedElBanna80](https://github.com/AhmedElBanna80) reported several ways `next/dynamic` differed from Next.js:

- In the App Router, `dynamic()` only adds a `<Suspense>` boundary when you pass `loading` or `ssr: false`. Otherwise the component suspends to the nearest boundary above it, so content no longer pops in after the shell.
- Components loaded from outside the Vite root, such as workspace packages in a monorepo, get their CSS and preload links during SSR instead of rendering unstyled until hydration. Packages in `transpilePackages` get them too.
- `useId()` values inside a dynamic component match between the server and the client.
- Text right before a dynamic component no longer gets an extra `<!-- -->` from its script preloads.

## Server Actions

`@vitejs/plugin-rsc` 0.5.37 ties the encrypted values an inline Server Action closes over to that action, as Next.js does. On older versions, a client could send one action's encrypted closure to another action on the same route. Vinext now requires 0.5.37. The same kind of check now applies to inline `"use cache"` functions.

Errors from actions match Next.js too. An action called with `fetch` that throws rejects in the browser with a `digest`, and is reported to `instrumentation.onRequestError`. A form submitted without JavaScript whose action throws gets a plain 500, rather than the nearest `error.tsx` with a 200 status.

Action modules are now evaluated outside the request that first loads them, so a `cookies()` or `headers()` call at module scope can't keep the first user's values for later requests in the same isolate.

## next-intl

next-intl's [App Router playground](https://github.com/amannn/next-intl/tree/main/examples/example-app-router-playground) and its [canonical App Router example](https://github.com/amannn/next-intl/tree/main/examples/example-app-router) now run in our CI against a Workers production build, using next-intl's own Playwright tests. The playground runs in seven configurations, covering localized pathnames, `basePath`, trailing slashes, domains and locale cookies. It found three bugs, all fixed in this release: a middleware rewrite that also carried `x-middleware-next` was ignored, `clientOutDir` didn't apply to the client build, and `next/image` didn't use the `basePath`. `vinext check` now reports next-intl as supported.

## Module Federation

The Module Federation team now publishes [`@module-federation/vinext`](https://github.com/module-federation/vinext), a wrapper around `@module-federation/vite` with defaults for Vinext. It shares React and React DOM as singletons and injects the host's federation startup into the client entry, since a Vinext app has no HTML entry to put it in. [@Nsttt](https://github.com/Nsttt) wrote the new [Module Federation guide](/docs/guides/module-federation) around it.

Getting a federated host and remote working turned up two fixes:

- Files a plugin writes outside `_next/static`, such as a host's `/remoteEntry.js`, returned 404 from `vinext start` for App Router apps and from Workers using the `runWorkerFirst` setting above, so the page never hydrated. Vinext now serves files from the client build before middleware runs, which matches Cloudflare's default of serving assets first.
- `@vitejs/plugin-rsc` 0.5.36 and 0.5.37 turn template-literal dynamic imports into glob imports during their scan build, and those fail to resolve. That broke federated builds. Vinext works around it on those two versions.

A federated host and remote now also run in CI against a Workers build.

## Other changes

- `vinext init --name` sets the Cloudflare Worker name. [@MattieTK](https://github.com/MattieTK) added it so tools like `@cloudflare/autoconfig` can pass the name the user picked.
- [@jeffreywescott](https://github.com/jeffreywescott) fixed RSC streams carrying large Flight rows. On a page with ~1.4 MB of client component props, the Worker spent about a second of CPU per request rescanning the row.
- `vinext start` and `vite dev` log uncaught errors and unhandled rejections instead of exiting, as `next start` does. [@jgeurts](https://github.com/jgeurts) stopped client disconnects from being reported to `onRequestError` as render failures.
- `<html data-scroll-behavior="smooth">` turns off smooth scrolling during route changes, as in Next.js. [@williamrogersdev](https://github.com/williamrogersdev) reported and fixed this for the App Router.
- `skipTrailingSlashRedirect` was parsed but ignored, and now works.
- [@wxpartners-web](https://github.com/wxpartners-web) reported that one cancelled request could stop the Static Assets response cache from serving prerendered pages for the rest of the isolate. That's fixed.
- Typed `cloudflare.config.ts` projects using Response Store in self-contained mode turned on Workers Cache for the whole Worker, so Cloudflare could serve application responses from its cache before the Worker ran. Every config Vinext generates now keeps it off except for the cache's own entrypoint. Rebuild and deploy to pick up the fix.
- Response Store answers a matching `If-None-Match` with a 304, as Next.js does, and a crawler's conditional request can no longer store an empty 304 for everyone else.
- `server-only` and `"use server"` modules are handled in browser Web Workers as they are in the rest of the client build. A Pages Router page that imports from middleware or an API route can no longer pull a `server-only` module into the client bundle.
- Pages Router builds work with `vite preview` on Node.
- `--warm-cache-include-fallbacks` never warmed fallback shells, and is now a deprecated no-op.

Thanks to everyone who contributed to 1.2: [@jeffreywescott](https://github.com/jeffreywescott), [@jgeurts](https://github.com/jgeurts), [@MattieTK](https://github.com/MattieTK), [@Nsttt](https://github.com/Nsttt) and [@williamrogersdev](https://github.com/williamrogersdev), and to [@AhmedElBanna80](https://github.com/AhmedElBanna80), [@Mauladen](https://github.com/Mauladen) and [@wxpartners-web](https://github.com/wxpartners-web) for their bug reports.

The full lists of changes are in the release notes for [vinext 1.2.0](https://github.com/cloudflare/vinext/releases/tag/vinext%401.2.0), [@vinext/cloudflare 1.1.1](https://github.com/cloudflare/vinext/releases/tag/%40vinext%2Fcloudflare%401.1.1) and [create-vinext-app 1.1.0](https://github.com/cloudflare/vinext/releases/tag/create-vinext-app%401.1.0).
31 changes: 31 additions & 0 deletions packages/cloudflare/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
# @vinext/cloudflare

## 1.1.1

Read the [Vinext 1.2 release post](https://vinext.dev/blog/vinext-1-2) for the highlights.

### Bug Fixes

#### Cloudflare

- don't share an in-flight static assets cache index fetch across requests (#3965)
- never warm through an http-only Worker route (#3931)
- make --warm-cache-include-fallbacks a no-op (#3906)
- answer a stored page's conditional request as Next.js does (#3832)
- never store a conditional request's 304 in the Response Store (#3827)
- keep Workers Cache off Worker-wide in every generated config (#3820)

#### Misc

- **Cache:** treat x-api-key as a credential for shared response caches (#3879)
- **PPR:** key fallback shells apart from the pages of their literal paths (#3891)
- **Cache:** key oversized cache identities by a SHA-256 digest (#3876)

### Performance

- **Cloudflare:** share the pending static assets cache index fetch across requests (#3972)
- **Cloudflare:** skip getStaticPaths when Workers Cache revalidates a stored Pages entry (#3960)
- **Pages Router:** skip getStaticPaths when Response Store regenerates a stored entry (#3959)

### Contributors

- @james-elicx

## 1.1.0

### Features
Expand Down
2 changes: 1 addition & 1 deletion packages/cloudflare/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vinext/cloudflare",
"version": "1.1.0",
"version": "1.1.1",
"description": "Cloudflare cache and image adapters for vinext.",
"homepage": "https://vinext.dev/",
"license": "MIT",
Expand Down
12 changes: 12 additions & 0 deletions packages/create-vinext-app/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# create-vinext-app

## 1.1.0

Read the [Vinext 1.2 release post](https://vinext.dev/blog/vinext-1-2) for the highlights.

### Features

- **Init:** add --name to set the Cloudflare Worker name (#3863)

### Contributors

- @MattieTK

## 1.0.0

### Features
Expand Down
2 changes: 1 addition & 1 deletion packages/create-vinext-app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "create-vinext-app",
"version": "1.0.0",
"version": "1.1.0",
"description": "Create a vinext app for Cloudflare Workers.",
"homepage": "https://vinext.dev/",
"license": "MIT",
Expand Down
Loading
Loading