* 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