* fix(pages): emit canonical next data script
* test(pages): expect canonical next data markup
* fix(pages): initialize canonical next data before routing
* test(pages): parse canonical next data markup
* fix(pages): preserve router bootstrap ordering
* test(pages): expect readiness bootstrap import
* fix(pages): guard readiness bootstrap without next data
* fix(pages-router): buffer SSR response for crawler/bot User-Agents
Pages Router edge-runtime SSR was streaming HTML to all clients, including
crawlers like Googlebot and Google-PageRenderer that cannot parse incrementally
flushed HTML. This caused the Next.js compat test
`streaming-ssr-edge/should not stream to crawlers or google pagerender bot`
to fail: bots received multi-chunk responses without an ETag.
Mirror Next.js's behaviour from `send-payload.ts`: detect crawler UAs via
`isPagesStreamingBot()` (matching Next.js's html-bots.ts + is-bot.ts regex
set), await the full compositeStream, generate a FNV-1a 52-bit ETag, and
return a single-chunk Response. Normal browser UAs continue to stream.
* fix(pages-router): add If-None-Match→304, ISR-HIT ETag, and UA-gating divergence comment
- Export `generatePagesETag` and add `etagMatches` helper (weak/strong ETag
comparison mirroring Next.js `sendEtagResponse` / RFC 7232 §3.2).
- `renderPagesPageResponse`: accept `ifNoneMatch` option; return 304 when the
buffered bot response's ETag matches, with an empty body.
- `resolvePagesPageData`: accept `userAgent` + `ifNoneMatch`; attach an ETag
to ISR cache-HIT responses for bot UAs (consistent with fresh-MISS path);
return 304 on HIT when If-None-Match matches.
- `pages-page-handler`: plumb `if-none-match` request header into both
`resolvePagesPageData` and `renderPagesPageResponse`.
- Add a clear divergence comment in `renderPagesPageResponse` documenting
that vinext gates buffering/ETag on the crawler UA rather than Next.js's
`isDynamic`-based flag.
- Tests: 8 new cases covering 304/200 on match/mismatch, non-bot 304 bypass,
weak-ETag comparison, ISR-HIT ETag attachment, and ISR-HIT 304.
* fix(pages-router): address ask-bonk review — STALE ETag/304, comment fixes, no-cache
- Hoist repeated bot ETag/304 logic into `applyBotETagAndCheck` helper so the
same code covers both the ISR cache-HIT and cache-STALE branches; previously
the STALE branch silently omitted ETag headers and 304 short-circuits for
crawler UAs
- Add `requestCacheControl` option to `RenderPagesPageResponseOptions` and
`ResolvePagesPageDataOptions`; skip the 304 short-circuit when the request
carries `Cache-Control: no-cache` (matches `fresh` package behaviour used by
Next.js's `sendEtagResponse`)
- Fix comment: ETag section header said "weak ETag by default" — Next.js
`generateETag()` defaults to strong ETag (`weak=false`); update to match
- Fix comment: `etagMatches` said Next.js uses the `etag` npm package with
`weak: true`; it actually uses the `fresh` package for staleness checking;
replace with accurate description
- Add tests: STALE-branch ETag attach, STALE 304, STALE non-bot skips 304,
and `no-cache` bypasses 304 on all three paths (fresh-MISS, HIT, STALE)
* docs(pages-router): correct RFC 7232 citation for weak ETag comparison (§3.2 → §2.3.2)
* docs(pages-router): note 304-on-dynamic consequence of UA-gated ETag divergence
* refactor(pages-router): share the no-cache bypass check between MISS and ISR bot paths
* refactor(pages-router): move fnv1a52 ETag hash into utils/hash.ts alongside fnv1a64
* fix(pages-router): run _document.getInitialProps with renderPage enhancers
Refs #1468.
Custom `pages/_document.tsx` files that override `getInitialProps` and
call `ctx.renderPage({ enhanceApp, enhanceComponent })` to wrap the
App/Component tree — the standard styled-components / emotion pattern
for SSR style collection — silently no-op'd. The shim type advertised
`renderPage`, but the SSR pipeline never invoked `getInitialProps` with
a real `renderPage`, so the enhancers never ran and `<Document>` never
saw the wrapped tree.
This wires the Pages Router `_document.getInitialProps` contract
through both the prod (`renderPagesPageResponse`) and dev
(`streamPageToResponse`) pipelines:
- A new `enhancePageElement(opts)` option exposes the App/Component
separation needed to apply `enhanceApp` / `enhanceComponent` from
the user's renderPage call. `pages-server-entry.ts` and the dev
server both wire it up to wrap `App({ Component, pageProps })`.
- When `DocumentComponent.getInitialProps` is defined, we invoke it
with a minimal `DocumentContext` shim that includes `renderPage` and
`defaultGetInitialProps`. `renderPage` rebuilds the React tree via
`enhancePageElement`, drains the React stream to a string, and
returns `{ html, head }`.
- If renderPage was called, the streaming body fallback is replaced
with the captured `html` so `_document` sees a complete body (matches
Next.js's `loadDocumentInitialProps` behaviour). Any `styles` element
returned by getInitialProps is rendered to HTML and appended to the
document head.
- If `getInitialProps` throws, returns a non-string `html`, or never
invokes renderPage, we fall through to the existing streaming path —
no user-visible regression for the common case where `_document` has
no `getInitialProps`.
Adds a focused regression in `tests/pages-page-response.test.ts` that
confirms `enhanceApp` and `enhanceComponent` both run and that the
enhanced tree appears in the rendered body. Mirrors
`.nextjs-ref/test/development/basic/styled-components/pages/_document.js`
and `.nextjs-ref/packages/next/src/server/render.tsx`
(`loadDocumentInitialProps` / `enhanceComponents`).
Test plan:
- `pnpm test tests/pages-page-response.test.ts`
- `pnpm test tests/document.test.ts tests/head.test.ts tests/shims.test.ts`
- `pnpm test tests/pages-router.test.ts`
- `pnpm test tests/entry-templates.test.ts`
- `pnpm run check`
* refactor(pages-router): consolidate _document renderPage onto shared helper
Address review feedback on the _document.getInitialProps + renderPage
contract:
- Extract a single shared runDocumentRenderPage() helper in
pages-document-initial-props.ts, used by both the prod
(pages-page-response.ts) and dev (dev-server.ts) SSR pipelines. The
duplicated inline implementation in dev-server.ts is removed (-86/+38).
- withScriptNonce now lives in one place (the shared helper); both
callers' enhancePageElement return the bare enhanced tree.
- Remove dead headFromDocument (returned but never consumed).
- Drop the redundant React.Fragment wrapper around enhancedElement.
- Reconcile with #1677's loadUserDocumentInitialProps: getInitialProps is
invoked at most once. A discriminated result (skipped/consumed/rendered)
signals when the fast path must be skipped so getInitialProps is not
called twice (which would re-throw a user error as a 500).
- Add edge-case tests: styles returned, getInitialProps throws (clean
fallback), renderPage never called.
* refactor(pages-router): address re-review nits on shared renderPage helper
- runDocumentRenderPage now skips the base-shim getInitialProps via the
BASE_GET_INITIAL_PROPS identity check (mirrors loadUserDocumentInitialProps)
so an unoverridden _document stays on the cheap fast path.
- Dev now forwards { pathname, query, asPath } as the DocumentContext, matching
the prod pipeline's parity.
- Add a test for renderPage being called but the underlying stream render
throwing (falls back cleanly to the streaming render).
* fix(pages-router): drop unused RunDocumentRenderPageResult export, document pageProps capture
- Remove the `export` on the internal `RunDocumentRenderPageResult` type
(knip flagged it as an unused export; it is only used as the helper's
return type within the same module).
- Document why `enhancePageElement` captures `pageProps` from the closure
rather than receiving it as a parameter (it is invoked once per request
with a single `pageProps`; only the enhancers vary).
* refactor(pages-router): address bonk re-review nits
- dev-server enhancePageElement: document the pageProps closure capture to
mirror the prod entry's comment
- prod renderStylesToString: render the collected styles fragment with the
plain stream renderer instead of the full <Document> shell renderer
- lazify pageElement construction so it is only built on the streaming path
(never used when renderPage already produced the body)
* 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.
* perf(pages): cache ISR misses from streamed render
Pages Router ISR cache misses currently render the page once for the live response and then render a second React tree to produce cache HTML. That doubles userland render, data reads, suspense work, and render-scoped side effects on every cold cache fill.
The cache writer now tees the already-rendered body stream, records the cache branch with streaming UTF-8 decoding, and writes the same shell and body that the client receives. Stream failures report through the Pages Router render error path and never persist partial HTML.
Tests cover the single-render cache fill, split UTF-8 chunks, failed stream writes, and the Pages Router fixture state contract.
* fix(pages): keep ISR cache error reporting non-fatal
* chore: rerun CI
Move renderToReadableStream() before buildPagesShellHtml() + clearSsrContext()
so that <Head> and other SSR state collectors (styled-jsx, useServerInsertedHTML)
are populated before they are read. This fixes a race condition where head styles
were silently dropped because they were collected before the page had finished
rendering.
Mirrors Next.js fix: vercel/next.js@9853944
The specific styled-jsx race condition (styledJsxInsertedHTML() called
concurrently with page render via Promise.all) does not exist in vinext because
vinext does not implement a styled-jsx style registry at all. However, the same
class of ordering bug affected vinext's Pages Router prod path where getSSRHeadHTML
was called before rendering completed.