Commit Graph

11 Commits

Author SHA1 Message Date
James Anderson bacee9ef52 fix(pages): emit canonical __NEXT_DATA__ JSON (#2043)
* 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
2026-06-15 23:16:23 +01:00
James Anderson 0fb9bd32bf fix(pages): apply Document renderPage enhancers (#2034)
* fix(pages): apply document renderPage enhancers

* fix(pages): propagate document render errors

* fix(pages): delegate document initial props

* fix(pages): render dev errors through document

* fix(pages): preserve document-provided html

* fix(pages): keep render page enhancer type internal

* fix(pages): buffer custom dev error documents
2026-06-15 22:53:40 +01:00
James Anderson f6b07eebcc fix(pages-router): buffer SSR response for crawler/bot User-Agents (#1876)
* 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
2026-06-11 00:30:13 +00:00
James Anderson f15267c8a1 fix(pages-router): run _document.getInitialProps with renderPage enhancers (#1682)
* 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)
2026-06-01 12:22:35 +01:00
James Anderson 0e64147176 fix(pages-router): pass revalidateReason to getStaticProps; default gSSP Cache-Control (#1573)
* 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.
2026-05-26 11:25:25 +01:00
Nathan Nguyen 3ebc28f0e1 fix: preserve Next.js cache headers for prerendered app pages (#1424)
* fix: preserve Next.js cache headers for prerendered app pages

* fix: address cache header review feedback
2026-05-22 10:08:08 +01:00
Nathan Nguyen 634808bea4 perf(pages): cache ISR misses from streamed render (#1068)
* 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
2026-05-05 19:43:07 +01:00
Nathan Nguyen f45fce00d5 fix(isr): honor route expire ceilings (#961)
* fix(isr): honor route expire ceilings

Track expireAt alongside revalidateAt in the memory and KV cache handlers so ISR entries past their expire ceiling become blocking misses instead of stale responses.

Plumb expireTime and request cacheLife expire values through App Router, Pages Router, prerender seeding, and cache writes while keeping generated entries as thin app-shape wiring over normal server modules.

Match Next.js cache-control semantics for finite stale-while-revalidate windows when an expire value is known.

* fix(isr): address cache life review follow-ups

* chore: address latest review follow-ups

* ci: pin vite-plus setup version

* Revert "ci: pin vite-plus setup version"

This reverts commit ee1d1d4e8db7b7406734414c53fa1b79f955a4d1.

* fix: avoid blocking ISR page streams on cache metadata

* fix: preserve headers for speculative cacheLife probes

* fix: preserve prerender cacheLife metadata

* fix: preserve legacy ISR cache metadata behavior

* fix: preserve prerender seed revalidate context

* fix: harden app page cache policy metadata

* fix: resolve app router prerender conflict
2026-05-02 19:54:25 +01:00
Divanshu Chauhan (divkix) f4d4ad6609 fix: serialize SSR render and head collection in Pages Router (#792) (#951)
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.
2026-04-29 10:33:55 +01:00
Stephen Zhou 14ff303cbd feat: csp support (#795)
* fix: support csp

* tweaks

* tweaks

* tweaks

* tweaks

* tweaks

* tweaks

* clean

* tweaks

* clean

* fix test

* tweaks

* fix lock

* fix test

* trigger ci

* tweaks

* tweaks
2026-04-09 09:14:13 +01:00
Steve Faulkner 99ae33f09c refactor: extract pages page response helpers (#657)
* refactor: extract pages page response helpers

* fix: preserve array-valued pages gssp headers
2026-03-22 18:39:15 -05:00