From 508085526d4fe86c6b638ad378d9bcef0b76a357 Mon Sep 17 00:00:00 2001 From: Aurora Scharff <66901228+aurorascharff@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:36:19 +0200 Subject: [PATCH 1/4] docs: adjust interactive app guide (#97558) ## What? Updates the Interactive Apps guide to show how an optimistic mutation handles an expected server error. ## Why? The optimistic card move automatically returns to the confirmed data when the write fails, but the guide did not show how to explain that failure to the user. A success toast would duplicate the visible card movement, so the example only adds feedback for the error case. ## How? - Return a structured error result when the task no longer exists. - Display that expected error in a toast while `useOptimistic` reverts the card. - Forward unexpected thrown errors to the nearest error boundary. --- docs/01-app/02-guides/interactive-apps.mdx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/01-app/02-guides/interactive-apps.mdx b/docs/01-app/02-guides/interactive-apps.mdx index e53225ce1b89..a2adde9e1069 100644 --- a/docs/01-app/02-guides/interactive-apps.mdx +++ b/docs/01-app/02-guides/interactive-apps.mdx @@ -434,6 +434,7 @@ Add `useOptimistic` with a reducer to remap the card's status on the current fra 'use client' import { startTransition, use, useOptimistic } from 'react' +import { toast } from 'sonner' import { updateStatus } from '@/features/task/task-actions' export function Board({ tasksPromise }) { @@ -449,7 +450,8 @@ export function Board({ tasksPromise }) { function handleDrop(targetStatus, taskId) { startTransition(async () => { moveTask({ taskId, status: targetStatus }) - await updateStatus(taskId, targetStatus) + const result = await updateStatus(taskId, targetStatus) + if (!result.success) toast.error(result.error) }) } @@ -467,9 +469,9 @@ export function Board({ tasksPromise }) { } ``` -The reducer maps over the task list and updates the status of the dragged card, leaving the rest unchanged. If a background refresh arrives mid-drag, for example from polling or another user's mutation, React re-runs the reducer with the updated base data so the optimistic move sits on top of fresh data. +The reducer maps over the task list and updates the status of the dragged card, leaving the rest unchanged. If a background refresh arrives mid-drag, for example from polling or another user's mutation, React re-runs the reducer with the updated base data so the optimistic move sits on top of fresh data. The Server Function returns an error result when the task no longer exists, which the client displays in a toast. -This step uses the standalone `startTransition` instead of `useTransition` because the hook's `isPending` would trigger the board fade from Step 3. The optimistic move already covers the visual feedback, so no pending indicator is needed. If the Server Function fails, the card reverts. +This step uses the standalone `startTransition` instead of `useTransition` because the hook's `isPending` would trigger the board fade from Step 3. The optimistic move already covers the visual feedback, so no pending indicator is needed. If the Server Function returns an expected error, the card reverts. Unexpected errors are forwarded to the nearest [error boundary](/docs/app/getting-started/error-handling). A card dragged from "Todo" to "In Progress" now lands in the target column the moment you release. @@ -724,11 +726,17 @@ The per-id tag `task-${id}` gives a single task its own handle. The broader `tas 'use server' import { updateTag } from 'next/cache' +import { updateTaskStatus } from '@/lib/db' export async function updateStatus(taskId: string, newStatus: Status) { - // …mutate + const updated = await updateTaskStatus(taskId, newStatus) + if (!updated) { + return { success: false as const, error: 'Task no longer exists' } + } + updateTag('tasks') updateTag(`task-${taskId}`) + return { success: true as const, status: newStatus } } ``` From 5c5daffedd06d434263b1847257636149e36baf7 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:57:53 +0200 Subject: [PATCH 2/4] Move preview props into separate manifest (#96004) Store/load the encryption keys at `.next/server/preview-props.json` (which are always needed) as opposed to including the giant `.next/prerender-manifest.json` The manifest is added to `required-server-file.json#files` so it keeps getting included in prod `.next/prerender-manifest.json` is unchanged for backwards compatiblity reasons This is work towards the goal of not including the one giant prerender-manifest at runtime in the serverless function --- .../next/src/build/adapter/build-complete.ts | 13 ++++--- packages/next/src/build/index.ts | 12 +++++- .../src/build/templates/app-page-runtime.ts | 6 ++- .../next/src/build/templates/app-route.ts | 5 ++- .../next/src/build/templates/edge-ssr-app.ts | 6 ++- packages/next/src/build/templates/edge-ssr.ts | 4 +- .../next/src/build/templates/pages-api.ts | 5 +-- .../helpers/create-incremental-cache.ts | 16 ++++---- packages/next/src/export/index.ts | 9 ++++- .../src/server/app-render/action-handler.ts | 3 +- packages/next/src/server/base-server.ts | 4 +- .../src/server/lib/incremental-cache/index.ts | 15 +++++--- .../src/server/lib/router-utils/filesystem.ts | 18 +++++---- .../lib/router-utils/setup-dev-bundler.ts | 14 ++++++- packages/next/src/server/next-server.ts | 37 +++++++++++++++---- .../route-modules/pages/pages-handler.ts | 5 ++- .../src/server/route-modules/route-module.ts | 28 +++++++++++--- packages/next/src/server/web/adapter.ts | 15 ++++---- packages/next/src/shared/lib/constants.ts | 1 + .../e2e/middleware-general/test/index.test.ts | 6 ++- .../test/index.test.ts | 6 ++- .../next-server-nft/next-server-nft.test.ts | 1 + .../required-server-files.test.ts | 4 +- .../generate-cache-key.test.ts | 28 ++++++++------ 24 files changed, 182 insertions(+), 79 deletions(-) diff --git a/packages/next/src/build/adapter/build-complete.ts b/packages/next/src/build/adapter/build-complete.ts index 5c7d6db9c12a..f9d295ce8331 100644 --- a/packages/next/src/build/adapter/build-complete.ts +++ b/packages/next/src/build/adapter/build-complete.ts @@ -59,6 +59,7 @@ import { generateRoutesManifest } from '../generate-routes-manifest' import { Bundler } from '../../lib/bundler' import { resolveCacheHandlerPathToFilesystem } from '../../lib/format-dynamic-import-path' import { InvariantError } from '../../shared/lib/invariant-error' +import type { __ApiPreviewProps } from '../../server/api-utils' interface SharedRouteFields { /** @@ -605,6 +606,7 @@ export async function handleBuildComplete({ nextVersion, hasStatic404, hasStatic500, + previewProps, routesManifest, serverPropsPages, hasNodeMiddleware, @@ -629,6 +631,7 @@ export async function handleBuildComplete({ nextVersion: string hasStatic404: boolean hasStatic500: boolean + previewProps: __ApiPreviewProps bundler: Bundler staticPages: Set hasNodeMiddleware: boolean @@ -847,7 +850,7 @@ export async function handleBuildComplete({ { type: 'header', key: 'x-prerender-revalidate', - value: prerenderManifest.preview.previewModeId, + value: previewProps.previewModeId, }, ], } @@ -1111,7 +1114,7 @@ export async function handleBuildComplete({ { type: 'header', key: 'x-prerender-revalidate', - value: prerenderManifest.preview.previewModeId, + value: previewProps.previewModeId, }, ], } @@ -1589,7 +1592,7 @@ export async function handleBuildComplete({ isAppPage && srcRoute !== '/_not-found' ? experimentalBypassFor : undefined, - bypassToken: prerenderManifest.preview.previewModeId, + bypassToken: previewProps.previewModeId, }, } // Classification describes the primary HTML or Route Handler body, @@ -1861,7 +1864,7 @@ export async function handleBuildComplete({ renderingMode, partialFallback: canEmitPartialFallback || undefined, bypassFor: isAppPage ? experimentalBypassFor : undefined, - bypassToken: prerenderManifest.preview.previewModeId, + bypassToken: previewProps.previewModeId, }, } @@ -2089,7 +2092,7 @@ export async function handleBuildComplete({ { type: 'cookie', key: '__prerender_bypass', - value: prerenderManifest.preview.previewModeId, + value: previewProps.previewModeId, }, { type: 'cookie', diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index b09eb6fca543..d41ca6bafeb8 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -83,6 +83,7 @@ import { FUNCTIONS_CONFIG_MANIFEST, DYNAMIC_CSS_MANIFEST, TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST, + PREVIEW_PROPS_MANIFEST, } from '../shared/lib/constants' import { UNDERSCORE_NOT_FOUND_ROUTE, @@ -414,6 +415,7 @@ export type PrerenderManifest = { routes: { [route: string]: PrerenderManifestRoute } dynamicRoutes: { [route: string]: DynamicPrerenderManifestRoute } notFoundRoutes: string[] + /** @deprecated only kept for the builder, use PreviewPropsManifest within Next.js itself */ preview: __ApiPreviewProps } @@ -507,6 +509,7 @@ function getPagesFallbackClassification( } export type SubresourceIntegrityManifest = Record +export type PreviewPropsManifest = __ApiPreviewProps type ManifestBuiltRoute = { /** @@ -2012,6 +2015,7 @@ export default async function build( path.relative(distDir, pagesManifestPath), BUILD_MANIFEST, PRERENDER_MANIFEST, + path.join(SERVER_DIRECTORY, PREVIEW_PROPS_MANIFEST), path.join(SERVER_DIRECTORY, FUNCTIONS_CONFIG_MANIFEST), path.join(SERVER_DIRECTORY, MIDDLEWARE_MANIFEST), path.join(SERVER_DIRECTORY, MIDDLEWARE_BUILD_MANIFEST + '.js'), @@ -4414,11 +4418,16 @@ export default async function build( version: 4, routes: {}, dynamicRoutes: {}, - preview: previewProps, notFoundRoutes: [], + preview: previewProps, }) } + await writeManifest( + path.join(distDir, 'server', PREVIEW_PROPS_MANIFEST), + previewProps + ) + // #endregion await writeImagesManifest(distDir, config) @@ -4549,6 +4558,7 @@ export default async function build( outputFileTracingRoot, hasNodeMiddleware, hasInstrumentationHook, + previewProps, adapterPath, pageKeys: pageKeys.pages, appPageKeys: emittedAppPageKeys, diff --git a/packages/next/src/build/templates/app-page-runtime.ts b/packages/next/src/build/templates/app-page-runtime.ts index 9f55ec77ffb4..34313659ede1 100644 --- a/packages/next/src/build/templates/app-page-runtime.ts +++ b/packages/next/src/build/templates/app-page-runtime.ts @@ -257,6 +257,7 @@ export function createAppPageEntrypoint({ interceptionRoutePatterns, deploymentId, clientAssetToken, + previewProps, } = prepareResult let { isOnDemandRevalidate } = prepareResult @@ -804,6 +805,7 @@ export function createAppPageEntrypoint({ (await routeModule.getIncrementalCache( req, nextConfig, + previewProps, prerenderManifest, isMinimalMode )) @@ -890,7 +892,7 @@ export function createAppPageEntrypoint({ crossOrigin: nextConfig.crossOrigin, trailingSlash: nextConfig.trailingSlash, images: nextConfig.images, - previewProps: prerenderManifest.preview, + previewProps, enableTainting: nextConfig.experimental.taint, reactMaxHeadersLength: nextConfig.reactMaxHeadersLength, @@ -1240,6 +1242,7 @@ export function createAppPageEntrypoint({ nextConfig, routeKind: RouteKind.APP_PAGE, isFallback: true, + previewProps, prerenderManifest, isRoutePPREnabled, responseGenerator: async () => @@ -1610,6 +1613,7 @@ export function createAppPageEntrypoint({ isRoutePPREnabled, req, nextConfig, + previewProps, prerenderManifest, waitUntil: ctx.waitUntil, isMinimalMode, diff --git a/packages/next/src/build/templates/app-route.ts b/packages/next/src/build/templates/app-route.ts index 73dd9398fa32..cb13c334a433 100644 --- a/packages/next/src/build/templates/app-route.ts +++ b/packages/next/src/build/templates/app-route.ts @@ -156,6 +156,7 @@ export async function handler( resolvedPathname, clientReferenceManifest, serverActionsManifest, + previewProps, } = prepareResult const normalizedSrcPage = normalizeAppPath(srcPage) @@ -221,6 +222,7 @@ export async function handler( (await routeModule.getIncrementalCache( req, nextConfig, + previewProps, prerenderManifest, isMinimalMode )) @@ -230,7 +232,7 @@ export async function handler( const context: AppRouteRouteHandlerContext = { params, - previewProps: prerenderManifest.preview, + previewProps, renderOpts: { experimental: { authInterrupts: Boolean(nextConfig.experimental.authInterrupts), @@ -401,6 +403,7 @@ export async function handler( cacheKey, routeKind: RouteKind.APP_ROUTE, isFallback: false, + previewProps, prerenderManifest, isRoutePPREnabled: false, isOnDemandRevalidate, diff --git a/packages/next/src/build/templates/edge-ssr-app.ts b/packages/next/src/build/templates/edge-ssr-app.ts index be760d0a1cd5..f70cdad431ad 100644 --- a/packages/next/src/build/templates/edge-ssr-app.ts +++ b/packages/next/src/build/templates/edge-ssr-app.ts @@ -83,6 +83,7 @@ async function requestHandler( nextConfig, buildManifest, prerenderManifest, + previewProps, reactLoadableManifest, subresourceIntegrityManifest, dynamicCssManifest, @@ -102,7 +103,7 @@ async function requestHandler( const botType = getBotType(req.headers.get('User-Agent') || '') const { isOnDemandRevalidate } = checkIsOnDemandRevalidate( req.headers, - prerenderManifest.preview + previewProps ) const closeController = new CloseController() @@ -149,7 +150,7 @@ async function requestHandler( crossOrigin: nextConfig.crossOrigin, trailingSlash: nextConfig.trailingSlash, images: nextConfig.images, - previewProps: prerenderManifest.preview, + previewProps: previewProps, enableTainting: nextConfig.experimental.taint, reactMaxHeadersLength: nextConfig.reactMaxHeadersLength, @@ -195,6 +196,7 @@ async function requestHandler( incrementalCache: await pageRouteModule.getIncrementalCache( baseReq, nextConfig, + previewProps, prerenderManifest, true ), diff --git a/packages/next/src/build/templates/edge-ssr.ts b/packages/next/src/build/templates/edge-ssr.ts index 07b0cfa32386..718e38e47a90 100644 --- a/packages/next/src/build/templates/edge-ssr.ts +++ b/packages/next/src/build/templates/edge-ssr.ts @@ -119,7 +119,7 @@ async function requestHandler( deploymentId, isNextDataRequest, buildManifest, - prerenderManifest, + previewProps, reactLoadableManifest, subresourceIntegrityManifest, dynamicCssManifest, @@ -155,7 +155,7 @@ async function requestHandler( ComponentMod: pageMod, pageConfig: pageMod.pageConfig, routeModule: pageMod.routeModule, - previewProps: prerenderManifest.preview, + previewProps, basePath: nextConfig.basePath, assetPrefix: nextConfig.assetPrefix, images: nextConfig.images, diff --git a/packages/next/src/build/templates/pages-api.ts b/packages/next/src/build/templates/pages-api.ts index e0955a1a6c0a..1c2e1d33724e 100644 --- a/packages/next/src/build/templates/pages-api.ts +++ b/packages/next/src/build/templates/pages-api.ts @@ -76,8 +76,7 @@ export async function handler( return } - const { query, params, prerenderManifest, routerServerContext } = - prepareResult + const { query, params, previewProps, routerServerContext } = prepareResult try { const method = req.method || 'GET' @@ -106,7 +105,7 @@ export async function handler( .__NEXT_TRUST_HOST_HEADER as any as boolean, // TODO: get this from from runtime env so manifest // doesn't need to load - previewProps: prerenderManifest.preview, + previewProps, propagateError: false, dev: routeModule.isDev, page: 'VAR_DEFINITION_PAGE', diff --git a/packages/next/src/export/helpers/create-incremental-cache.ts b/packages/next/src/export/helpers/create-incremental-cache.ts index 7f13c795178e..ab8285fd674d 100644 --- a/packages/next/src/export/helpers/create-incremental-cache.ts +++ b/packages/next/src/export/helpers/create-incremental-cache.ts @@ -53,23 +53,25 @@ export async function createIncrementalCache({ } } + let previewProps = { + previewModeEncryptionKey: '', + previewModeId: '', + previewModeSigningKey: '', + } const incrementalCache = new IncrementalCache({ dev: false, requestHeaders: requestHeaders || {}, flushToDisk, maxMemoryCacheSize: cacheMaxMemorySize, fetchCacheKeyPrefix, - getPrerenderManifest: () => ({ + previewProps, + prerenderManifest: { version: 4, routes: {}, dynamicRoutes: {}, - preview: { - previewModeEncryptionKey: '', - previewModeId: '', - previewModeSigningKey: '', - }, notFoundRoutes: [], - }), + preview: previewProps, + }, fs: nodeFs, serverDistDir: path.join(distDir, 'server'), CurCacheHandler: CacheHandler, diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 794dff89478b..e197d0bb8c78 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -8,6 +8,7 @@ import type { import { createStaticWorker, type PrerenderManifest, + type PreviewPropsManifest, type StaticWorker, } from '../build' import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin' @@ -43,6 +44,7 @@ import { APP_PATH_ROUTES_MANIFEST, ROUTES_MANIFEST, FUNCTIONS_CONFIG_MANIFEST, + PREVIEW_PROPS_MANIFEST, } from '../shared/lib/constants' import loadConfig from '../server/config' import type { ExportPathMap } from '../server/config-shared' @@ -260,6 +262,11 @@ async function exportAppImpl( !options.pages && (require(join(distDir, SERVER_DIRECTORY, PAGES_MANIFEST)) as PagesManifest) + let previewProps: DeepReadonly | undefined + try { + previewProps = require(join(distDir, 'server', PREVIEW_PROPS_MANIFEST)) + } catch {} + let prerenderManifest: DeepReadonly | undefined try { prerenderManifest = require(join(distDir, PRERENDER_MANIFEST)) @@ -476,7 +483,7 @@ async function exportAppImpl( // Start the rendering process const renderOpts: WorkerRenderOptsPartial = { - previewProps: prerenderManifest?.preview, + previewProps, isBuildTimePrerendering: true, assetPrefix: nextConfig.assetPrefix.replace(/\/$/, ''), distDir, diff --git a/packages/next/src/server/app-render/action-handler.ts b/packages/next/src/server/app-render/action-handler.ts index 1156f88da65e..3a4dc927fe8d 100644 --- a/packages/next/src/server/app-render/action-handler.ts +++ b/packages/next/src/server/app-render/action-handler.ts @@ -428,8 +428,7 @@ async function createRedirectRenderResult( ) forwardedHeaders.set( NEXT_CACHE_REVALIDATE_TAG_TOKEN_HEADER, - workStore.incrementalCache?.prerenderManifest?.preview?.previewModeId || - '' + workStore.incrementalCache?.previewProps.previewModeId || '' ) } diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 9f333b6173f6..da46058be8d6 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -31,6 +31,7 @@ import type { ManifestRewriteRoute, ManifestRoute, PrerenderManifest, + PreviewPropsManifest, } from '../build' import type { ClientReferenceManifest } from '../build/webpack/plugins/flight-manifest-plugin' import type { NextFontManifest } from '../build/webpack/plugins/next-font-manifest-plugin' @@ -377,6 +378,7 @@ export default abstract class Server< url?: string }): Promise protected abstract getPrerenderManifest(): DeepReadonly + protected abstract getPreviewProps(): DeepReadonly protected abstract getNextFontManifest(): | DeepReadonly | undefined @@ -570,7 +572,7 @@ export default abstract class Server< trailingSlash: this.nextConfig.trailingSlash, poweredByHeader: this.nextConfig.poweredByHeader, generateEtags, - previewProps: this.getPrerenderManifest().preview, + previewProps: this.getPreviewProps(), basePath: this.nextConfig.basePath, images: this.nextConfig.images, optimizeCss: this.nextConfig.experimental.optimizeCss, diff --git a/packages/next/src/server/lib/incremental-cache/index.ts b/packages/next/src/server/lib/incremental-cache/index.ts index 20a978324209..c523353db671 100644 --- a/packages/next/src/server/lib/incremental-cache/index.ts +++ b/packages/next/src/server/lib/incremental-cache/index.ts @@ -35,6 +35,7 @@ import { getPreviouslyRevalidatedTags } from '../../server-utils' import { workAsyncStorage } from '../../app-render/work-async-storage.external' import { createPromiseWithResolvers } from '../../../shared/lib/promise-with-resolvers' import { areTagsExpired, areTagsStale } from './tags-manifest.external' +import type { __ApiPreviewProps } from '../../api-utils' export interface CacheHandlerContext { fs?: CacheFs @@ -144,6 +145,7 @@ export class IncrementalCache implements IncrementalCacheType { readonly disableForTestmode?: boolean readonly cacheHandler?: CacheHandler readonly hasCustomCacheHandler: boolean + readonly previewProps: DeepReadonly<__ApiPreviewProps> readonly prerenderManifest: DeepReadonly readonly requestHeaders: Record readonly allowedRevalidateHeaderKeys?: string[] @@ -170,7 +172,8 @@ export class IncrementalCache implements IncrementalCacheType { serverDistDir, requestHeaders, maxMemoryCacheSize, - getPrerenderManifest, + previewProps, + prerenderManifest, fetchCacheKeyPrefix, CurCacheHandler, allowedRevalidateHeaderKeys, @@ -183,7 +186,8 @@ export class IncrementalCache implements IncrementalCacheType { allowedRevalidateHeaderKeys?: string[] requestHeaders: IncrementalCache['requestHeaders'] maxMemoryCacheSize?: number - getPrerenderManifest: () => DeepReadonly + previewProps: DeepReadonly<__ApiPreviewProps> + prerenderManifest: DeepReadonly fetchCacheKeyPrefix?: string CurCacheHandler?: typeof CacheHandler }) { @@ -232,14 +236,15 @@ export class IncrementalCache implements IncrementalCacheType { this[minimalModeKey] = minimalMode this.requestHeaders = requestHeaders this.allowedRevalidateHeaderKeys = allowedRevalidateHeaderKeys - this.prerenderManifest = getPrerenderManifest() + this.previewProps = previewProps + this.prerenderManifest = prerenderManifest this.cacheControls = new SharedCacheControls(this.prerenderManifest) this.fetchCacheKeyPrefix = fetchCacheKeyPrefix let revalidatedTags: string[] = [] if ( requestHeaders[PRERENDER_REVALIDATE_HEADER] === - this.prerenderManifest?.preview?.previewModeId + this.previewProps.previewModeId ) { this.isOnDemandRevalidate = true } @@ -247,7 +252,7 @@ export class IncrementalCache implements IncrementalCacheType { if (minimalMode) { revalidatedTags = this.revalidatedTags = getPreviouslyRevalidatedTags( requestHeaders, - this.prerenderManifest?.preview?.previewModeId + this.previewProps.previewModeId ) } diff --git a/packages/next/src/server/lib/router-utils/filesystem.ts b/packages/next/src/server/lib/router-utils/filesystem.ts index 986b85c34f47..a9b453e193d3 100644 --- a/packages/next/src/server/lib/router-utils/filesystem.ts +++ b/packages/next/src/server/lib/router-utils/filesystem.ts @@ -1,7 +1,7 @@ import type { FunctionsConfigManifest, ManifestRoute, - PrerenderManifest, + PreviewPropsManifest, RoutesManifest, } from '../../../build' import type { NextConfigRuntime } from '../../config-shared' @@ -49,7 +49,7 @@ import { FUNCTIONS_CONFIG_MANIFEST, MIDDLEWARE_MANIFEST, PAGES_MANIFEST, - PRERENDER_MANIFEST, + PREVIEW_PROPS_MANIFEST, ROUTES_MANIFEST, } from '../../../shared/lib/constants' import { normalizePathSep } from '../../../shared/lib/page-path/normalize-path-sep' @@ -346,7 +346,11 @@ export async function setupFsCheck(opts: { } const routesManifestPath = path.join(distDir, ROUTES_MANIFEST) - const prerenderManifestPath = path.join(distDir, PRERENDER_MANIFEST) + const previewPropsManifestPath = path.join( + distDir, + 'server', + PREVIEW_PROPS_MANIFEST + ) const middlewareManifestPath = path.join( distDir, 'server', @@ -369,11 +373,9 @@ export async function setupFsCheck(opts: { await fs.readFile(routesManifestPath, 'utf8') ) as RoutesManifest - previewProps = ( - JSON.parse( - await fs.readFile(prerenderManifestPath, 'utf8') - ) as PrerenderManifest - ).preview + previewProps = JSON.parse( + await fs.readFile(previewPropsManifestPath, 'utf8') + ) as PreviewPropsManifest const middlewareManifest = JSON.parse( await fs.readFile(middlewareManifestPath, 'utf8').catch(() => '{}') diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index d01d9822efb4..ad18411a59b8 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -53,6 +53,8 @@ import { TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST, ROUTES_MANIFEST, PRERENDER_MANIFEST, + PREVIEW_PROPS_MANIFEST, + SERVER_DIRECTORY, } from '../../../shared/lib/constants' import { getMiddlewareRouteMatcher } from '../../../shared/lib/router/utils/middleware-route-matcher' @@ -329,6 +331,17 @@ async function startWatcher( JSON.stringify(routesManifest) ) + const previewPropsManifestPath = path.join( + distDir, + SERVER_DIRECTORY, + PREVIEW_PROPS_MANIFEST + ) + fs.mkdirSync(path.join(distDir, SERVER_DIRECTORY), { recursive: true }) + await fs.promises.writeFile( + previewPropsManifestPath, + JSON.stringify(opts.fsChecker.previewProps, null, 2) + ) + const prerenderManifestPath = path.join(distDir, PRERENDER_MANIFEST) await fs.promises.writeFile( prerenderManifestPath, @@ -338,7 +351,6 @@ async function startWatcher( routes: {}, dynamicRoutes: {}, notFoundRoutes: [], - preview: opts.fsChecker.previewProps, }, null, 2 diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index f86cf4dabfc8..8fcc0e7c4a5e 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -11,7 +11,11 @@ import { import type { MiddlewareManifest } from '../build/webpack/plugins/middleware-plugin' import type RenderResult from './render-result' import type { FetchEventResult } from './web/types' -import type { PrerenderManifest, RoutesManifest } from '../build' +import type { + PrerenderManifest, + PreviewPropsManifest, + RoutesManifest, +} from '../build' import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin' import type { NextParsedUrlQuery, @@ -49,6 +53,7 @@ import { NEXT_FONT_MANIFEST, UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, FUNCTIONS_CONFIG_MANIFEST, + PREVIEW_PROPS_MANIFEST, } from '../shared/lib/constants' import { findDir } from '../lib/find-pages-dir' import { NodeNextRequest, NodeNextResponse } from './base-http/node' @@ -459,7 +464,8 @@ export default class NextNodeServer extends BaseServer< maxMemoryCacheSize: this.nextConfig.cacheMaxMemorySize, flushToDisk: !this.minimalMode && this.nextConfig.experimental.isrFlushToDisk, - getPrerenderManifest: () => this.getPrerenderManifest(), + previewProps: this.getPreviewProps(), + prerenderManifest: this.getPrerenderManifest(), CurCacheHandler: CacheHandler, }) } @@ -2000,17 +2006,34 @@ export default class NextNodeServer extends BaseServer< return result.finished } - private _cachedPreviewManifest: DeepReadonly | undefined + private _cachedPrerenderManifest: DeepReadonly | undefined protected getPrerenderManifest(): DeepReadonly { - if (this._cachedPreviewManifest) { - return this._cachedPreviewManifest + if (this._cachedPrerenderManifest) { + return this._cachedPrerenderManifest } - this._cachedPreviewManifest = loadManifest( + this._cachedPrerenderManifest = loadManifest( join(/* turbopackIgnore: true */ this.distDir, PRERENDER_MANIFEST) ) - return this._cachedPreviewManifest + return this._cachedPrerenderManifest + } + + private _cachedPreviewPropsManifest: PreviewPropsManifest | undefined + protected getPreviewProps(): PreviewPropsManifest { + if (this._cachedPreviewPropsManifest) { + return this._cachedPreviewPropsManifest + } + + this._cachedPreviewPropsManifest = loadManifest( + join( + /* turbopackIgnore: true */ this.distDir, + 'server', + PREVIEW_PROPS_MANIFEST + ) + ) as PreviewPropsManifest + + return this._cachedPreviewPropsManifest } private _cachedPrefetchHints: Record | undefined diff --git a/packages/next/src/server/route-modules/pages/pages-handler.ts b/packages/next/src/server/route-modules/pages/pages-handler.ts index 2d939b16438c..483c34b5870a 100644 --- a/packages/next/src/server/route-modules/pages/pages-handler.ts +++ b/packages/next/src/server/route-modules/pages/pages-handler.ts @@ -150,6 +150,7 @@ export const getHandler = ({ serverFilesManifest, reactLoadableManifest, prerenderManifest, + previewProps, isDraftMode, isOnDemandRevalidate, revalidateOnlyGenerated, @@ -306,7 +307,7 @@ export const getHandler = ({ reactLoadableManifest, assetPrefix: nextConfig.assetPrefix, - previewProps: prerenderManifest.preview, + previewProps, images: nextConfig.images as any, nextConfigOutput: nextConfig.output, optimizeCss: Boolean(nextConfig.experimental.optimizeCss), @@ -476,6 +477,7 @@ export const getHandler = ({ incrementalCache: await routeModule.getIncrementalCache( req, nextConfig, + previewProps, prerenderManifest, isMinimalMode ), @@ -538,6 +540,7 @@ export const getHandler = ({ revalidateOnlyGenerated, waitUntil: ctx.waitUntil, responseGenerator: responseGenerator, + previewProps, prerenderManifest, isMinimalMode, }) diff --git a/packages/next/src/server/route-modules/route-module.ts b/packages/next/src/server/route-modules/route-module.ts index d2fccc4fffc3..29f62ee47bef 100644 --- a/packages/next/src/server/route-modules/route-module.ts +++ b/packages/next/src/server/route-modules/route-module.ts @@ -8,6 +8,7 @@ import type { ParsedUrlQuery } from 'node:querystring' import type { UrlWithParsedQuery } from 'node:url' import type { PrerenderManifest, + PreviewPropsManifest, RequiredServerFilesManifest, } from '../../build' import type { DevRoutesManifest } from '../lib/router-utils/setup-dev-bundler' @@ -21,6 +22,7 @@ import { NEXT_FONT_MANIFEST, PREFETCH_HINTS, PRERENDER_MANIFEST, + PREVIEW_PROPS_MANIFEST, REACT_LOADABLE_MANIFEST, ROUTES_MANIFEST, SERVER_FILES_MANIFEST, @@ -37,7 +39,7 @@ import { removePathPrefix } from '../../shared/lib/router/utils/remove-path-pref import { getServerUtils } from '../server-utils' import { detectDomainLocale } from '../../shared/lib/i18n/detect-domain-locale' import { getHostname } from '../../shared/lib/get-hostname' -import { checkIsOnDemandRevalidate } from '../api-utils' +import { checkIsOnDemandRevalidate, type __ApiPreviewProps } from '../api-utils' import type { PreviewData } from '../../types' import type { BuildManifest } from '../get-page-files' import type { ReactLoadableManifest } from '../load-components' @@ -222,6 +224,7 @@ export abstract class RouteModule< dynamicCssManifest: any prefetchHintsManifest: Record | undefined interceptionRoutePatterns: RegExp[] + previewProps: __ApiPreviewProps } { let result if (process.env.NEXT_RUNTIME === 'edge') { @@ -244,6 +247,7 @@ export abstract class RouteModule< version: 4, preview: getEdgePreviewProps(), } as const, + previewProps: getEdgePreviewProps(), routesManifest: { version: 4, caseSensitive: Boolean(process.env.__NEXT_CASE_SENSITIVE_ROUTES), @@ -293,6 +297,7 @@ export abstract class RouteModule< const [ routesManifest, prerenderManifest, + previewProps, buildManifest, fallbackBuildManifest, reactLoadableManifest, @@ -317,6 +322,12 @@ export abstract class RouteModule< manifest: PRERENDER_MANIFEST, shouldCache: !this.isDev, }), + loadManifestFromRelativePath({ + projectDir, + distDir: this.distDir, + manifest: `server/${PREVIEW_PROPS_MANIFEST}`, + shouldCache: !this.isDev, + }), loadManifestFromRelativePath({ projectDir, distDir: this.distDir, @@ -416,6 +427,7 @@ export abstract class RouteModule< routesManifest, nextFontManifest, prerenderManifest, + previewProps, serverFilesManifest, reactLoadableManifest, clientReferenceManifest: (clientReferenceManifest as any) @@ -476,6 +488,7 @@ export abstract class RouteModule< public async getIncrementalCache( req: IncomingMessage | BaseNextRequest, nextConfig: NextConfigRuntime, + previewProps: DeepReadonly<__ApiPreviewProps>, prerenderManifest: DeepReadonly, isMinimalMode: boolean ): Promise { @@ -520,7 +533,8 @@ export abstract class RouteModule< fetchCacheKeyPrefix: nextConfig.experimental.fetchCacheKeyPrefix, maxMemoryCacheSize: nextConfig.cacheMaxMemorySize, flushToDisk: !isMinimalMode && nextConfig.experimental.isrFlushToDisk, - getPrerenderManifest: () => prerenderManifest, + previewProps, + prerenderManifest, CurCacheHandler: CacheHandler, }) @@ -659,6 +673,7 @@ export abstract class RouteModule< nextConfig: NextConfigRuntime routerServerContext?: RouterServerContext[string] interceptionRoutePatterns?: any + previewProps: __ApiPreviewProps } | undefined > { @@ -699,7 +714,7 @@ export abstract class RouteModule< { spanName: 'load route manifests' }, () => this.loadManifests(srcPage, absoluteProjectDir) ) - const { routesManifest, prerenderManifest, serverFilesManifest } = manifests + const { routesManifest, previewProps, serverFilesManifest } = manifests const { basePath, i18n, rewrites } = routesManifest @@ -1013,7 +1028,7 @@ export abstract class RouteModule< } const { isOnDemandRevalidate, revalidateOnlyGenerated } = - checkIsOnDemandRevalidate(req.headers, prerenderManifest.preview) + checkIsOnDemandRevalidate(req.headers, previewProps) let isDraftMode = false let previewData: PreviewData @@ -1026,7 +1041,7 @@ export abstract class RouteModule< previewData = tryGetPreviewData( req, res, - prerenderManifest.preview, + previewProps, Boolean(multiZoneDraftMode) ) isDraftMode = previewData !== false @@ -1134,6 +1149,7 @@ export abstract class RouteModule< cacheKey, routeKind, isFallback, + previewProps, prerenderManifest, isRoutePPREnabled, isOnDemandRevalidate, @@ -1147,6 +1163,7 @@ export abstract class RouteModule< cacheKey: string | null routeKind: RouteKind isFallback?: boolean + previewProps: DeepReadonly<__ApiPreviewProps> prerenderManifest: DeepReadonly isRoutePPREnabled?: boolean isOnDemandRevalidate?: boolean @@ -1176,6 +1193,7 @@ export abstract class RouteModule< incrementalCache: await this.getIncrementalCache( req, nextConfig, + previewProps, prerenderManifest, isMinimalMode ), diff --git a/packages/next/src/server/web/adapter.ts b/packages/next/src/server/web/adapter.ts index 4eba602270b2..4baecf7f5c67 100644 --- a/packages/next/src/server/web/adapter.ts +++ b/packages/next/src/server/web/adapter.ts @@ -243,14 +243,13 @@ export async function adapter( dev: process.env.NODE_ENV === 'development', requestHeaders: params.request.headers as any, - getPrerenderManifest: () => { - return { - version: -1 as any, // letting us know this doesn't conform to spec - routes: {}, - dynamicRoutes: {}, - notFoundRoutes: [], - preview: getEdgePreviewProps(), - } + previewProps: getEdgePreviewProps(), + prerenderManifest: { + version: -1 as any, // letting us know this doesn't conform to spec + routes: {}, + dynamicRoutes: {}, + notFoundRoutes: [], + preview: getEdgePreviewProps(), }, }) } diff --git a/packages/next/src/shared/lib/constants.ts b/packages/next/src/shared/lib/constants.ts index cebb3295e9e0..7f58c1fffe63 100644 --- a/packages/next/src/shared/lib/constants.ts +++ b/packages/next/src/shared/lib/constants.ts @@ -95,6 +95,7 @@ export const EXPORT_MARKER = 'export-marker.json' export const EXPORT_DETAIL = 'export-detail.json' export const PRERENDER_MANIFEST = 'prerender-manifest.json' export const PREFETCH_HINTS = 'prefetch-hints.json' +export const PREVIEW_PROPS_MANIFEST = 'preview-props.json' export const ROUTES_MANIFEST = 'routes-manifest.json' export const IMAGES_MANIFEST = 'images-manifest.json' export const SERVER_FILES_MANIFEST = 'required-server-files' diff --git a/test/e2e/middleware-general/test/index.test.ts b/test/e2e/middleware-general/test/index.test.ts index 546f220bb83f..96e38e6dcdd1 100644 --- a/test/e2e/middleware-general/test/index.test.ts +++ b/test/e2e/middleware-general/test/index.test.ts @@ -273,8 +273,10 @@ describe('Middleware Runtime', () => { it('should not run middleware for on-demand revalidate', async () => { const bypassToken = ( - await fs.readJSON(join(next.testDir, '.next/prerender-manifest.json')) - ).preview.previewModeId + await fs.readJSON( + join(next.testDir, '.next/server/preview-props.json') + ) + ).previewModeId const res = await fetchViaHTTP(next.url, '/ssg/first', undefined, { headers: { diff --git a/test/e2e/middleware-trailing-slash/test/index.test.ts b/test/e2e/middleware-trailing-slash/test/index.test.ts index bb44b610df23..bc3e0654cd64 100644 --- a/test/e2e/middleware-trailing-slash/test/index.test.ts +++ b/test/e2e/middleware-trailing-slash/test/index.test.ts @@ -136,8 +136,10 @@ describe('Middleware Runtime trailing slash', () => { it('should not run middleware for on-demand revalidate', async () => { const bypassToken = ( - await fs.readJSON(join(next.testDir, '.next/prerender-manifest.json')) - ).preview.previewModeId + await fs.readJSON( + join(next.testDir, '.next/server/preview-props.json') + ) + ).previewModeId const res = await fetchViaHTTP(next.url, '/ssg/first/', undefined, { headers: { diff --git a/test/production/next-server-nft/next-server-nft.test.ts b/test/production/next-server-nft/next-server-nft.test.ts index 19115f2b0277..46fa4d20e30f 100644 --- a/test/production/next-server-nft/next-server-nft.test.ts +++ b/test/production/next-server-nft/next-server-nft.test.ts @@ -624,6 +624,7 @@ async function readNormalizedNFT(next, name) { "./.next/server/next-font-manifest.json", "./.next/server/pages-manifest.json", "./.next/server/prefetch-hints.json", + "./.next/server/preview-props.json", "./.next/server/server-reference-manifest.js", "./.next/server/server-reference-manifest.json", "/node_modules/@swc/helpers/cjs/_interop_require_default.cjs", diff --git a/test/production/standalone-mode/required-server-files/required-server-files.test.ts b/test/production/standalone-mode/required-server-files/required-server-files.test.ts index de124d4fe521..37aae5af3cf3 100644 --- a/test/production/standalone-mode/required-server-files/required-server-files.test.ts +++ b/test/production/standalone-mode/required-server-files/required-server-files.test.ts @@ -515,8 +515,8 @@ describe('required server files', () => { it('should not 404 for onlyGenerated on-demand revalidate in minimal mode', async () => { const previewProps = JSON.parse( - await next.readFile('standalone/.next/prerender-manifest.json') - ).preview + await next.readFile('standalone/.next/server/preview-props.json') + ) const res = await fetchViaHTTP( appPort, diff --git a/test/unit/incremental-cache/generate-cache-key.test.ts b/test/unit/incremental-cache/generate-cache-key.test.ts index 24ca7886e603..4a7e6b098ca1 100644 --- a/test/unit/incremental-cache/generate-cache-key.test.ts +++ b/test/unit/incremental-cache/generate-cache-key.test.ts @@ -4,18 +4,22 @@ function createCache() { return new IncrementalCache({ dev: false, requestHeaders: {}, - getPrerenderManifest: () => - ({ - version: 4, - routes: {}, - dynamicRoutes: {}, - notFoundRoutes: [], - preview: { - previewModeId: 'id', - previewModeSigningKey: 'key', - previewModeEncryptionKey: 'key', - }, - }) as any, + previewProps: { + previewModeId: 'id', + previewModeSigningKey: 'key', + previewModeEncryptionKey: 'key', + }, + prerenderManifest: { + version: 4, + routes: {}, + dynamicRoutes: {}, + notFoundRoutes: [], + preview: { + previewModeId: 'id', + previewModeSigningKey: 'key', + previewModeEncryptionKey: 'key', + }, + }, }) } From 1e6423eae70cf3228a60b503c120b95d991227e8 Mon Sep 17 00:00:00 2001 From: Jiwon Choi Date: Wed, 19 Aug 2026 18:37:53 +0200 Subject: [PATCH 3/4] docs: generateMetadata values should be serializable with use cache (#97551) ### Why? generateMetadata can be marked with use cache, but Cache Function return values must be serializable. The metadataBase examples use URL objects, so the interaction needs co-located guidance for cached metadata. ### How? Add a Good to know note that recommends returning metadataBase as a string, such as with url.toString(), and links to the use cache serialization requirements. Runtime behavior is unchanged. --------- Co-authored-by: Joseph --- docs/01-app/03-api-reference/04-functions/generate-metadata.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/01-app/03-api-reference/04-functions/generate-metadata.mdx b/docs/01-app/03-api-reference/04-functions/generate-metadata.mdx index 064e5dd77c7a..1474707b9c20 100644 --- a/docs/01-app/03-api-reference/04-functions/generate-metadata.mdx +++ b/docs/01-app/03-api-reference/04-functions/generate-metadata.mdx @@ -421,6 +421,7 @@ export const metadata = { > **Good to know**: > > - `metadataBase` is typically set in root `app/layout.js` to apply to URL-based `metadata` fields across all routes. +> - When `generateMetadata` uses `'use cache'`, its return value must be serializable. `URL` instances are not supported by Cache Functions, so values such as `metadataBase` should be returned as strings (for example, `url.toString()`). See [`use cache` serialization requirements](/docs/app/api-reference/directives/use-cache#serialization). > - All URL-based `metadata` fields that require absolute URLs can be configured with a `metadataBase` option. > - `metadataBase` can contain a subdomain e.g. `https://app.acme.com` or base path e.g. `https://acme.com/start/from/here` > - If a `metadata` field provides an absolute URL, `metadataBase` will be ignored. From 9b5eee09cfd85b014860e02f1c33c8a4a8f9bc11 Mon Sep 17 00:00:00 2001 From: Jude Gao Date: Wed, 19 Aug 2026 14:05:34 -0400 Subject: [PATCH 4/4] Fix stale cross-references in skills/ (#97566) --- skills/next-cache-components-adoption/SKILL.md | 4 ++-- .../reference/real-app-patterns.md | 2 +- .../reference/red-test-robustness.md | 2 +- skills/next-dev-loop/SKILL.md | 5 ----- skills/next-partial-prefetching-adoption/SKILL.md | 8 ++++---- 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/skills/next-cache-components-adoption/SKILL.md b/skills/next-cache-components-adoption/SKILL.md index fe738614217f..b5cf8f2a3793 100644 --- a/skills/next-cache-components-adoption/SKILL.md +++ b/skills/next-cache-components-adoption/SKILL.md @@ -77,7 +77,7 @@ In preference order: 1. **[`next-dev-loop`](https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop) — strongly preferred.** Cross-checks `/_next/mcp` against the live browser via `agent-browser` and surfaces both compile and runtime issues in one pass. The diagnostics (React tree, suspense boundaries, console + network) are richer than poking at `next dev` by hand. - Install it before starting the loop. Don't wait until you hit something `next dev` alone can't explain. Run: + Install it before starting the loop. Don't wait until you hit something `next dev` alone can't explain. It ships alongside this skill, so check whether it is already available first, and install it only if it is not: ```bash npx skills add https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop @@ -234,6 +234,6 @@ When the loop has run on every feature — every remaining `instant = false` sit The work below is optional and lives in the docs — link the user to them and let them decide which to take on next. Don't walk these through inside this skill. - [Sweep for more instant navigations](./references/dev-only-validations.md) — an optional follow-up once adoption is done, never required. A passing build is not the last word, because dev validates every route on each page load (simulating both page loads and client navigations) and catches what the build's first-error exit and descendant shadowing skipped. Offer it as the smaller path to instant navigation for a user who doesn't want to adopt Partial Prefetching. Adopting Partial Prefetching (below) runs the same kind of loop and meets these insights anyway, so recommend both and let the user pick which, or whether. The reference is the loop to execute. -- [`next-partial-prefetching-adoption`](https://github.com/vercel/next.js/tree/canary/skills/next-partial-prefetching-adoption) — the follow-up skill that adopts Partial Prefetching: it enables `partialPrefetching` and audits every `` against a decision table (or adopts incrementally with the flag off, driven by the `link-prefetch-partial` insight). It sequences this the same way this skill sequences Cache Components, but the insights are dev-only, so it's a browser click-through, not a build loop. Recommended after instant navigation, since those fixes feed directly into how much of each route the shell can prefetch. Concepts live in the [Adopting Partial Prefetching guide](https://nextjs.org/docs/app/guides/adopting-partial-prefetching). +- [`next-partial-prefetching-adoption`](https://github.com/vercel/next.js/tree/canary/skills/next-partial-prefetching-adoption) — the follow-up skill that adopts Partial Prefetching: it enables `partialPrefetching` and audits every `` against a decision table (or adopts incrementally with the flag off, driven by the `instant-link-prefetch-partial` insight). It sequences this the same way this skill sequences Cache Components, but the insights are dev-only, so it's a browser click-through, not a build loop. Recommended after instant navigation, since those fixes feed directly into how much of each route the shell can prefetch. Concepts live in the [Adopting Partial Prefetching guide](https://nextjs.org/docs/app/guides/adopting-partial-prefetching). - [Prevent regressions with e2e tests](https://nextjs.org/docs/app/guides/instant-navigation#prevent-regressions-with-e2e-tests) — the `@next/playwright` [`instant()`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/instant#testing-instant-navigation) helper asserts on the UI that's available immediately on navigation, so regressions surface in CI. Recommend it once a route is instant: `next-dev-loop` confirms it _now_; an `instant()` test keeps it that way. - [`next-cache-components-optimizer`](https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-optimizer) — a separate skill that grows each route's static shell so more of the page prerenders and less streams in. Pure optimization, not part of adoption. diff --git a/skills/next-cache-components-optimizer/reference/real-app-patterns.md b/skills/next-cache-components-optimizer/reference/real-app-patterns.md index 63572a188ee7..31fa2e04189d 100644 --- a/skills/next-cache-components-optimizer/reference/real-app-patterns.md +++ b/skills/next-cache-components-optimizer/reference/real-app-patterns.md @@ -76,7 +76,7 @@ The shell prerenders as if authorized (the session read suspends before `redirec ## Initial-load shell vs soft-navigation shell -The `test-template.md` specs drive a `` click for soft navigations and `page.goto()` for initial loads. The two shells can differ for the same route: +The `../test-template.md` specs drive a `` click for soft navigations and `page.goto()` for initial loads. The two shells can differ for the same route: > **The initial-load shell can show less than the soft-navigation shell when a layout above the shared boundary awaits un-enumerated `params`/`searchParams`.** An initial load re-runs every layout from the root; if a parent layout does `await props.params` and that segment has no `generateStaticParams`, the param suspends on the initial load and its whole subtree drops out of the shell. A soft navigation does not re-render that parent and already has the params. Symptom: an element present after a `` click is missing after `goto`. diff --git a/skills/next-cache-components-optimizer/reference/red-test-robustness.md b/skills/next-cache-components-optimizer/reference/red-test-robustness.md index ec5dfc4ac6ad..bf106faaed9b 100644 --- a/skills/next-cache-components-optimizer/reference/red-test-robustness.md +++ b/skills/next-cache-components-optimizer/reference/red-test-robustness.md @@ -139,7 +139,7 @@ Two defenses; use both: (SKILL.md phases 0 and A). Do not trust a pass from a build where it is not set. 2. **Make the test self-validating**: for any route with deferred content, also assert that the deferred content is gated under the lock, not only that the shell is present - (`test-template.md`, self-validating variant). If the lock did not engage, the content is + (`../test-template.md`, self-validating variant). If the lock did not engage, the content is already present and `toHaveCount(0)` fails. The gated half holds under the lock for both navigation types regardless of warm state: the diff --git a/skills/next-dev-loop/SKILL.md b/skills/next-dev-loop/SKILL.md index 29de455a82c0..f3d253b66475 100644 --- a/skills/next-dev-loop/SKILL.md +++ b/skills/next-dev-loop/SKILL.md @@ -181,8 +181,3 @@ Close the session with the same session and restore context: `agent-browser --session "$SESSION" --restore close`. `close` saves that session's cookies and storage so the next loop's `--restore` open keeps the user logged in. Leave `next dev` up for the next loop. - ---- - -`next-dev-loop-` siblings (e.g. `next-dev-loop-rsc`, `next-dev-loop-debug`) -assume this preflight already ran; they pick up at the loop. diff --git a/skills/next-partial-prefetching-adoption/SKILL.md b/skills/next-partial-prefetching-adoption/SKILL.md index 0b0f784d8e1a..82696915d50b 100644 --- a/skills/next-partial-prefetching-adoption/SKILL.md +++ b/skills/next-partial-prefetching-adoption/SKILL.md @@ -6,7 +6,7 @@ description: > Partial Prefetching, flip the `partialPrefetching` flag, opt routes in with `export const prefetch = 'partial'`, audit `` calls, or resolve the - link-prefetch-partial and instant-shell-url-data insights. + instant-link-prefetch-partial and instant-shell-url-data insights. --- # next-partial-prefetching-adoption @@ -23,7 +23,7 @@ Talk to the user in terms of what they'll see — PRs, features, and how the app - **Next.js 16.3 or later.** `partialPrefetching`, the `prefetch` route segment config, and the prefetch insights all land there. -- **A browser you can drive.** Install [`next-dev-loop`](https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop) before starting (`npx skills add https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop`). Install it without asking — it's a tool, not a product change — and don't assume it's blocked: verify a real blocker (no network, no npm, read-only filesystem) before falling back, and name it in your report. Link prefetches fire when a link renders and enters the viewport, and shell validation fires on navigation — neither is reachable from `curl` or the build. If the app is webpack-pinned, drive a browser directly (`agent-browser`, Playwright) — you lose the framework cross-checks, not the insights; they're still in the overlay and the dev log. +- **A browser you can drive.** Install [`next-dev-loop`](https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop) before starting, unless it is already available — it ships alongside this skill (`npx skills add https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop`). Install it without asking — it's a tool, not a product change — and don't assume it's blocked: verify a real blocker (no network, no npm, read-only filesystem) before falling back, and name it in your report. Link prefetches fire when a link renders and enters the viewport, and shell validation fires on navigation — neither is reachable from `curl` or the build. If the app is webpack-pinned, drive a browser directly (`agent-browser`, Playwright) — you lose the framework cross-checks, not the insights; they're still in the overlay and the dev log. - **A runnable app.** Verification runs against `next dev` for the insight sweep and a production `next build`/`next start` for prefetching (prefetching is prod-only), so the app has to boot in both. If it reads a database or required env at import (e.g. an `env.ts` that throws on a missing `DATABASE_URL`), confirm it starts — with the real environment, or local data you stand up — before step 1. An app that won't run can't be swept or verified. @@ -49,7 +49,7 @@ Every insight has a docs page — open it. Fetch the linked page for every disti ## step 1: audit `` (before enabling) -If `partialPrefetching: true` is already set in `next.config.ts`, the app is adopted — skip to [step 3](#step-3-sweep-for-url-data-insights-after-enabling). Otherwise work the audit with the global flag **off**, adopting each destination with `export const prefetch = 'partial'` — enabling the flag first would mark every route adopted and silence the [`link-prefetch-partial`](https://nextjs.org/docs/messages/instant-link-prefetch-partial) insight this audit runs on. Ask the user how to ship it, in the language of PRs: +If `partialPrefetching: true` is already set in `next.config.ts`, the app is adopted — skip to [step 3](#step-3-sweep-for-url-data-insights-after-enabling). Otherwise work the audit with the global flag **off**, adopting each destination with `export const prefetch = 'partial'` — enabling the flag first would mark every route adopted and silence the [`instant-link-prefetch-partial`](https://nextjs.org/docs/messages/instant-link-prefetch-partial) insight this audit runs on. Ask the user how to ship it, in the language of PRs: - **One branch** — the whole audit in one change, with the flag enabled and the codemod run at the end (step 2). - **Route by route** — each adopted destination ships as its own PR. The insight still fires for the destinations you haven't reached, a live worklist, and step 2 comes after the last one. @@ -60,7 +60,7 @@ Enumerate the prefetch sites across the whole source tree, not only `app/` — t Then, for each one: -1. **Click each `` in `next dev`.** The insight fires at navigation time, not when the link prefetches, so a link sitting in the viewport won't trip it — you have to navigate through it. This click is _verification_: it confirms the insight fires before you adopt and clears after. Imperative `router.prefetch()` sites have no equivalent insight, so audit them from source and verify them in production ([step 4](#step-4-verify)). Without a browser, skip the click and adopt from [`link-prefetch-partial`](https://nextjs.org/docs/messages/instant-link-prefetch-partial) and the audit table below — the destination's structure tells you the row, and type-check gates the edit — then leave the live confirmation for the hand-off. +1. **Click each `` in `next dev`.** The insight fires at navigation time, not when the link prefetches, so a link sitting in the viewport won't trip it — you have to navigate through it. This click is _verification_: it confirms the insight fires before you adopt and clears after. Imperative `router.prefetch()` sites have no equivalent insight, so audit them from source and verify them in production ([step 4](#step-4-verify)). Without a browser, skip the click and adopt from [`instant-link-prefetch-partial`](https://nextjs.org/docs/messages/instant-link-prefetch-partial) and the audit table below — the destination's structure tells you the row, and type-check gates the edit — then leave the live confirmation for the hand-off. 2. **Adopt the destination.** Add the temporary route config with a link to the migration guide. That clears the insight for every link pointing at it: ```tsx