* fix(pages): preserve app props for gssp requests
* docs(pages): update _next/data comment to reflect full props envelope
* fix(pages-router): preserve _app.getInitialProps during stale ISR regeneration
Stale ISR background regeneration called getStaticProps directly and rendered
HTML and __NEXT_DATA__ with only the page's pageProps, dropping app-level
props from _app.getInitialProps. The foreground path already loads
_app.getInitialProps first and merges its props, but the background
regeneration path skipped that step.
Extract a shared loadPagesAppInitialRenderProps helper from the production
foreground path and reuse it during background regeneration in both
pages-page-data.ts and dev-server.ts. This ensures regenerated pages carry the
same full props envelope as the original render.
Add a regression test in tests/pages-page-data.test.ts.
* fix(pages-router): skip _app.getInitialProps on ISR cache hits
The foreground render path ran _app.getInitialProps before consulting the
ISR cache, so fresh and stale cache hits executed userland App data code
even though the cached HTML was returned verbatim. That wasted work on the
hot path and ran side-effecting App code whose result the cached response
never reflected.
The eager App initial-props load happened up front, ahead of the HIT and
STALE short-circuits. Defer it until the request will actually render:
load it lazily inside the getServerSideProps / getStaticProps /
getInitialProps branches, after the cache short-circuits return.
Stale-while-revalidate already re-runs it inside the regeneration closure,
so regenerated HTML still carries app-level props. The dev SSR handler and
the production page-data resolver are updated in lockstep to preserve
dev/prod parity.
Tests cover the fresh-HIT skip and the stale foreground-skip vs
regeneration-run split at the page-data resolver boundary.
* refactor(pages-router): extract dev App getInitialProps loader into a tested helper
The dev SSR handler ran _app.getInitialProps inside an inline closure that
mutated captured pageProps/renderProps and read res directly, so its lazy
load-only-on-render behavior could only be covered by a heavyweight
integration test that vi.doMock'd the isr-cache and file-matcher modules —
a mock cascade with little signal.
Move the decision into loadDevAppInitialProps in pages-get-initial-props.ts.
It takes the App component, req/res, and an injected AppTree builder (keeping
React out of the module) and returns a discriminated result —
skip / response-sent / render — that the caller applies. The dev-server
closure is now thin glue: build the React tree, delegate, apply the result.
Adds focused unit tests that drive the helper through its real boundary
(genuine getInitialProps functions, plain req/res inputs) — no module mocks:
the skip path, App/page prop merging, pageProps normalization, the
response-sent short-circuit, and the router/ctx wiring passed to userland.
* fix(pages): preserve app props across data requests
* fix(pages): preserve fallback cache expiry
* fix(pages): preserve dev app context during ISR
* fix(pages): hydrate middleware rewrite fallbacks
* fix(pages): retarget middleware data rewrites
* fix(pages): preserve rewrite query on navigation
* fix(pages): finalize rewrite navigation state
* fix(pages): resolve rewrite data targets consistently
* fix(pages): await static props during regeneration
* fix(pages): preserve original query in resolved url
* Revert "fix(pages): await static props during regeneration"
This reverts commit cbdcdf65cd.
* test(app): cover rewrite params in app search
---------
Co-authored-by: James <james@eli.cx>
* fix(pages-router): block fallback shell for crawler UAs on prerender (#1543)
When `getStaticPaths` returns `fallback: true` and an unlisted path is hit
by a known bot/crawler, vinext was serving the `Loading...` shell so the
crawler indexed the fallback HTML instead of the real page. Mirror Next.js
`pages-handler.ts` and flip `isFallback` to false for bot User-Agents so
the request renders synchronously with real props.
- Add `isBotUserAgent` helper (DOM + HTML bot regex) in `utils/html-limited-bots.ts`,
matching Next.js `shared/lib/router/utils/is-bot.ts`.
- Plumb `userAgent` + `htmlLimitedBots` into `resolvePagesPageData` and
flip the fallback bit when the UA matches.
- Apply the same flip in the dev `createSSRHandler` for parity.
- Targeted unit + integration regression tests covering Googlebot,
Bingbot, DuckDuckBot, YandexBot, facebookexternalhit, and a normal
Chrome UA control. Ported from `.nextjs-ref/test/e2e/prerender-crawler.test.ts`.
Refs #1543.
* fix(pages-router): preserve array user-agent bot detection
* fix(pages-router): set x-nextjs-deployment-id on SSG _next/data responses
Pages Router `/_next/data/<buildId>/<page>.json` responses were missing the
`x-nextjs-deployment-id` response header that Next.js sets on these routes
when a deployment ID is configured. The client router reads this header to
detect when a new deployment has been rolled out and triggers a hard
navigation for deployment-skew protection.
Root cause: the `isDataReq` short-circuit path in `pages-page-handler.ts`
that builds the JSON envelope (`buildNextDataJsonResponse`) never injected
the header. Next.js's `pages-handler.ts` sets `NEXT_NAV_DEPLOYMENT_ID_HEADER`
on every `isNextDataRequest` response (success, redirect, and notFound cases).
Fix: read `process.env.__VINEXT_DEPLOYMENT_ID || process.env.NEXT_DEPLOYMENT_ID`
(the runtime-available deployment ID, inlined at build time by Vite's define
transform) and set `x-nextjs-deployment-id` on the `_next/data` response
headers when a deployment ID is configured.
Maps to test: `pages ssg data deployment skew - hard navigate when a new
deployment occurs > header with deployment id > header is set on data routes`.
Fixes#1829
* fix(pages-router): complete x-nextjs-deployment-id parity for _next/data (bonk #1829)
Address all three ask-bonk findings on PR #1863:
1. Incomplete parity — add x-nextjs-deployment-id to redirect and notFound _next/data
exits in pages-page-data.ts (buildPagesRedirectResponse, buildPagesDataNotFoundResponse).
Thread deploymentId through ResolvePagesPageDataOptions and pass it from
pages-page-handler.ts, matching the success-path pattern already in that file.
2. Dev/prod parity — patch dev-server.ts to set x-nextjs-deployment-id on all three
_next/data exits: the writeGsspRedirect isDataReq path, the getStaticPaths/
getServerSideProps/getStaticProps notFound isDataReq paths, and the success
JSON envelope short-circuit (isDataReq block). Mirrors the prod handler.
3. Add regression tests asserting x-nextjs-deployment-id is present on _next/data
success / redirect / notFound responses in both pages-page-data.test.ts (unit,
via deploymentId option) and pages-page-handler.test.ts (integration, via
process.env.__VINEXT_DEPLOYMENT_ID), and absent when no env var is set.
* fix(pages-router): add x-nextjs-deployment-id to wrong-buildId/route-miss _next/data 404s (#1829)
Fixes the deployment-skew gap identified in review: `buildNextDataNotFoundResponse` now
includes the `x-nextjs-deployment-id` header so all three wrong-buildId/route-miss exits
(normalizePagesDataRequest, prod-server Node path, pages-page-handler route-miss) surface
the deployment ID to the client router. Adds unit tests for the new header on all relevant
exits.
* fix(pages-router): set deployment-id header on remaining two dev-mode _next/data 404 exits (#1829)
Patches the stale-buildId 404 in the vite-dev middleware (index.ts) and
the route-miss data 404 in createSSRHandler (dev-server.ts) to set
x-nextjs-deployment-id, matching prod parity via buildNextDataNotFoundResponse().
* test(pages-router): cover dev-mode _next/data deployment-id header exits
Drive the two dev-only exits flagged in review through the real fixture
dev server: the wrong-buildId data 404 in the plugin middleware
(index.ts) and createSSRHandler's route-miss data 404 (dev-server.ts),
plus the dev success path and the no-deployment-id case.
* fix(pages-router): skip deployment-id header on /_error and /500 data success responses
Next.js pages-handler.ts guards the success-path x-nextjs-deployment-id
injection with `!isErrorPage && !is500Page`. Mirror that exclusion in
both the prod success short-circuit (pages-page-handler.ts) and the dev
one (dev-server.ts), and lock it in with a prod unit test (/_error and
/500) plus a dev integration test (/500).
* fix(pages): render custom errors for notFound results
Pages getStaticProps and getServerSideProps notFound HTML results returned the built-in 404 response directly. That skipped the configured pages/404 or pages/_error render path, so _error.getInitialProps never received ctx.req.url or ctx.asPath for the original request.
The page-data resolver now returns a typed notFound signal for HTML requests while preserving JSON-shaped 404 data responses. The generated Pages entry rerenders the configured not-found route with the original asPath, and Pages getInitialProps is loaded through a shared typed helper for prod and dev parity.
* fix(pages): address PR review feedback on getInitialProps binding, err context, and response-sent handling
- Call getInitialProps with component as this receiver
- Thread caught errors through to _error.getInitialProps(ctx.err)
- Check headersSent || writableEnded in dev server after getInitialProps
- Add tests: gSSP notFound, explicit 404, data JSON 404, err on throw, this binding
* refactor(pages): use isResponseSent helper in resolvePagesPageData for sent-check consistency
The prod block in resolvePagesPageData checked only res.headersSent, while
the helper's isResponseSent and the dev server check both headersSent and
writableEnded. The prod PagesReqResResponse only declares headersSent (its
getter is backed by the ended flag), so this is functionally correct in
prod, but the asymmetry is hard to read next to the helper and a future
change to the prod response shape would silently break sent-detection.
Export isResponseSent from pages-get-initial-props and use it at both
gSSP and gIP call sites so the prod resolver reuses the same
sent-detection logic as the helper.
Also adds tests for the writableEnded-only path and a focused unit test
for isResponseSent covering both prod-shaped and node-shaped res mocks.
* perf(pages): gate getInitialProps req/res allocation behind presence check
The getInitialProps prod block in resolvePagesPageData unconditionally
called createGsspReqRes() for every static page without gSP/gSSP, even
when the page had no getInitialProps either. This allocated a req/res
pair and an unresolved responsePromise that was immediately discarded.
Export hasPagesGetInitialProps from pages-get-initial-props.ts and gate
the prod block on it, keeping the hot path for plain static pages
allocation-free.
When `getStaticProps` or `getServerSideProps` returned props containing
non-JSON-serializable values (e.g. `Date`, `Map`, class instances), vinext
silently rendered an empty page instead of surfacing an error. This ports
Next.js's `isSerializableProps` check from `packages/next/src/lib/is-serializable-props.ts`
and invokes it from `pages-page-data.ts` and `dev-server.ts` after the
redirect/notFound short-circuits, mirroring `render.tsx` (~lines 982 and
1200). The thrown `SerializableError` is caught by the existing per-request
error boundary, which returns a 500 with a Next.js-style message naming the
offending key.
* fix(pages): render masked basePath error routes
Pages Router route misses under basePath returned generic HTML and masked client navigations to /404 or /_error could fetch the visible as-path instead of the error route. This diverged from Next.js when applications used next/router to preserve a friendly URL while rendering the error component.
The implementation assumed the browser URL and the component route could always collapse to one fetch target. Preserve the masked history URL, fetch the Pages error route as the HTML target, allow its 404 response to hydrate, and render pages/404 for production route misses with the correct status and client module metadata.
* test(pages): keep error navigation coverage out of vitest browser
Vitest integration CI does not install Playwright browsers, so launching Chromium from tests/pages-router.test.ts made the PR fail deterministically even though the upstream deploy harness passed.
Keep the browser-level verification in the upstream deploy-suite run and cover vinext locally at the lower boundaries: production 404 rendering through HTTP and Pages Router masked error-route navigation through the router shim. The generated Pages entry now also shares one SSR manifest module lookup helper between asset tag collection and client module URL resolution.
* fix(prerender): keep custom 404 out of generic page loop
* fix(pages): fall back to _error on route misses
* fix(pages): document the catch-all guard on matchRoute("/404")
The pattern check prevents a dynamic catch-all route (e.g. [...slug]) from
being used as the 404 fallback. Without it, matchRoute can return a
catch-all match and silently hijack the error rendering path.
* fix(pages): preserve error navigation rewrite order
* fix(pages): limit fallback rewrites to route misses
* fix(deploy): skip deferred error rendering for data requests
* fix(pages): preserve ISR response status
* fix(pages): keep module metadata in ISR regeneration
* fix(pages-router): pass revalidateReason to getStaticProps context
Pages Router `getStaticProps` now receives `context.revalidateReason`
matching Next.js: `'build'` during build-time prerendering,
`'on-demand'` when the dispatch is signalled as an explicit revalidation,
and `'stale'` for stale-while-revalidate background regeneration and
runtime cache-miss requests.
Closes#1462
* fix(pages-router): default gSSP Cache-Control in prod + JSON data paths
Mirrors the dev-server branch added earlier in this PR. Production HTML
gSSP responses (renderPagesPageResponse) and the _next/data JSON envelope
(pages-server-entry isDataReq) now default to
"private, no-cache, no-store, max-age=0, must-revalidate" when the user
did not set Cache-Control via res.setHeader, matching Next.js's
pages-handler.ts (revalidate: 0 → getCacheControlHeader). Closes the
dev/prod parity gap flagged by bigbonk.
Also fixes the broken `gsspRes.headers` access in the isDataReq path
(should be `getHeaders()`) so user-set headers actually propagate, and
notes that isOnDemandRevalidate is forward-looking plumbing (no
caller sets it yet — res.revalidate() isn't implemented).
* test(pages-router): fix misaligned continuation comment
Drop the extra leading space on the JSDoc continuation lines flagged by
bigbonk so the block aligns with the surrounding tests.
* fix(404): align default not-found copy with Next.js
The Pages Router previously emitted `<h1>404 - Page not found</h1>` and the
App Router emitted plain-text `This page could not be found` (no period) when
no user-defined 404 boundary was present. Next.js renders
`This page could not be found.` (with trailing period) for both routers.
Closes#1454.
* fix(404): address bot review — charset attribute and h1 line-height
- Use HTML attribute name 'charset' (not the JSX 'charSet') in the raw HTML template.
- Drop stray 'line-height:49px' on the Pages Router h1 — Next.js's _error.tsx inherits 48px from the parent div.
* fix(pages-router): render loading shell for unlisted `fallback: true` paths
PR #1394 fixed the post-rewrite static-vs-dynamic priority sub-problem
from #1331 and explicitly tracked `fallback: true` rewrite behaviour
separately. When `getStaticPaths` returns `fallback: true` and a request
hits a path not in the pre-rendered list, vinext was SSRing the page
fully — running `getStaticProps` and emitting `isFallback: false` in
`__NEXT_DATA__`. Next.js instead ships a loading shell with empty props
and `useRouter().isFallback === true`, then the client hydrates by
fetching the data URL. The upstream regression test
`should rewrite to fallback: true page successfully` in
`test/e2e/middleware-rewrites/test/index.test.ts` asserts "Loading..."
in the SSR HTML, which vinext could not produce.
Thread an `isFallback` signal through `resolvePagesPageData`,
`buildPagesNextDataScript`, the Pages Router SSR context, and both the
prod (`pages-server-entry.ts`) and dev (`dev-server.ts`) renderers:
- `getStaticProps` and `getServerSideProps` are skipped on the fallback
render (matches Next.js `render.tsx`'s `if (isSSG && !isFallback)`
gate around `getStaticProps`).
- Data requests (`/_next/data/<buildId>/<page>.json`) still resolve real
props so the client can swap in after the shell ships.
- `useRouter().isFallback` reads from the SSR context server-side and
from `__NEXT_DATA__.isFallback` client-side — both flip to true on
the shell render.
The dev-server comment that claimed Next.js dev "also renders fully"
was incorrect; Next.js's `pages-handler.ts` invokes `doRender` with
`isFallback: true` for dev fallback responses, so dev now serves the
shell too.
Tests:
- Updated `tests/pages-router.test.ts` "SSR renders unlisted path with
getStaticPaths fallback: true" — previously documented the deviation;
now asserts the loading shell, empty props, and `isFallback: true`.
- Added a counterpart test that the data URL for the same unlisted
path still returns real props from `getStaticProps`.
- Added a production-side regression test mirroring the upstream
"should rewrite to fallback: true page successfully" expectation.
Refs #1331
* test(pages-page-data): include isFallback: false in normalized render assertion
The render branch of `resolvePagesPageData` now returns `isFallback`
alongside the other fields (added in the parent commit for the
`fallback: true` loading shell). Update the strict-shape `toEqual` in
the cache-miss unit test so it matches the new return shape.
Next.js exposes `params` as `null` (effectively — via `params || null`
or `params ? await params : null` user-side) to route handlers and
`getServerSideProps`/`getStaticProps` on routes with no dynamic
segments. vinext was passing an empty object, which is truthy and
breaks user code that branches on `params`.
The fix computes a user-facing params value at each dispatch boundary
(`null` when `route.isDynamic === false`, otherwise the matched
object) while leaving internal bookkeeping — navigation context, query
merging, `useParams()` — on the empty-object shape that those
consumers expect.
References the Next.js test suite assertions:
- test/e2e/app-dir/app-routes/app-custom-routes.test.ts
"does not provide params to routes without dynamic parameters"
- test/e2e/edge-pages-support/index.test.ts
"should have correct query/params on index"
Closes#1350