Skip to content

feat(nextjs): add cache-safe optimization handoff support#379

Open
Charles Hudson (phobetron) wants to merge 1 commit into
mainfrom
NT-3708_implement-ssg-isr-esr-handoff-support
Open

feat(nextjs): add cache-safe optimization handoff support#379
Charles Hudson (phobetron) wants to merge 1 commit into
mainfrom
NT-3708_implement-ssg-isr-esr-handoff-support

Conversation

@phobetron

@phobetron Charles Hudson (phobetron) commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements explicit, cache-safe Optimization SDK handoff support for Next.js static, ISR, request-time, and edge rendering flows.

This PR adds framework-neutral handoff primitives in Core, browser handoff hydration in Web, React handoff roots, canonical Next.js handoff APIs, an edge-safe Next.js runtime, a Node request handoff helper, updated reference implementations, E2E coverage, and public/internal documentation for the supported rendering modes.

Request-derived profile handoffs now fail fast when paired with public or static cache scopes, keeping private request state out of shared cache paths.

Jira: NT-3708

Why

Optimized content rendering needs to support more than browser-owned runtime state. This PR makes the state handoff explicit for:

  • static routes that render known optimization selections at build time
  • ISR routes that cache public optimization permutations
  • request-time routes that hand private request state to the browser
  • edge routes that own request analytics and profile persistence before browser hydration
  • analytics-only browser hydration when optimized markup has already been rendered outside the browser runtime

The implementation separates:

  • optimization selection state
  • cache-safety metadata
  • browser hydration behavior
  • initial page event ownership
  • profile/private request state
  • customer-owned public permutation state

Review map

Suggested review order:

  1. Core model:

    • packages/universal/core-sdk/src/handoff.ts
    • packages/universal/core-sdk/src/handoff.test.ts
  2. Browser hydration/runtime behavior:

    • packages/web/web-sdk/src/handoff.ts
    • packages/web/web-sdk/src/analytics.ts
    • packages/web/web-sdk/src/handoff-state.ts
    • packages/web/web-sdk/src/ContentfulOptimization.ts
    • packages/web/web-sdk/src/*handoff*.test.ts
    • packages/web/web-sdk/src/analytics.test.ts
    • packages/web/web-sdk/src/ContentfulOptimization.test.ts
  3. React Web integration:

    • packages/web/frameworks/react-web-sdk/src/root/OptimizationRoot.tsx
    • packages/web/frameworks/react-web-sdk/src/root/OptimizationAnalyticsRoot.tsx
    • packages/web/frameworks/react-web-sdk/src/provider/OptimizationProvider.tsx
    • packages/web/frameworks/react-web-sdk/src/auto-page/useAutoPageEmitter.ts
    • related tests under packages/web/frameworks/react-web-sdk/src/**
  4. Next.js public APIs and edge behavior:

    • packages/web/frameworks/nextjs-sdk/src/handoff.ts
    • packages/web/frameworks/nextjs-sdk/src/edge.ts
    • packages/web/frameworks/nextjs-sdk/src/cache-middleware.ts
    • packages/web/frameworks/nextjs-sdk/src/forwarded-request-headers.ts
    • packages/web/frameworks/nextjs-sdk/src/app-router-server.tsx
    • App Router and Pages Router server/client updates
  5. Reference implementations and E2E evidence:

    • implementations/nextjs-sdk_app-router/**
    • implementations/nextjs-sdk_pages-router/**
    • implementations/web-sdk_angular/src/app/services/optimization.ts
    • lib/e2e-web/e2e/handoff.spec.ts
  6. Documentation:

    • documentation/concepts/optimization-handoff-and-cache-safe-rendering.md
    • documentation/guides/rendering-personalized-nextjs-routes-with-static-isr-and-edge-handoffs.md
    • updated Next.js App Router and Pages Router guides
    • documentation/internal/sdk-knowledge/**

Public API changes

Core SDK

Adds @contentful/optimization-core handoff support:

  • OptimizationSelectionState
  • OptimizationCacheScope
  • OptimizationCacheMetadata
  • OptimizationHandoff
  • OptimizationCacheSafetyWarning
  • createSelectionFingerprint
  • createOptimizationCacheKey
  • resolveEntriesForSelections
  • createHandoffFromSelections
  • getOptimizationCacheSafetyWarnings

These APIs let SDKs and customer integrations describe the optimization state used during server/static/edge rendering and attach cache-safety metadata to the rendered output.

Web SDK

Adds browser handoff and analytics-only runtime support:

  • ContentOptimizationHydrationMode
  • OptimizationHydrationMode
  • ContentOptimizationHandoff
  • AnalyticsOptimizationHandoff
  • BrowserOptimizationHandoff
  • hydrateOptimizationHandoff
  • createOptimizationAnalyticsRuntime
  • hydrateOptimizationAnalyticsHandoff

New subpath exports:

  • @contentful/optimization-web/handoff
  • @contentful/optimization-web/analytics

ContentfulOptimization.trackCurrentPage({ initialPageEvent: "skip" }) marks the supplied route key as accepted without emitting a browser page event. Framework route trackers use that handoff signal only for the SSR/edge/request-owned initial mounted route, so later browser-owned SPA navigations emit normally.

React Web SDK

Adds handoff-aware root support:

  • OptimizationRoot accepts handoff state, hydration behavior, route identity, and initial page event payload builders.
  • OptimizationProvider hydrates changed handoff state into an existing live runtime so persistent roots can consume new server handoffs after mount.
  • OptimizationAnalyticsRoot supports analytics-only hydration.
  • React Web roots accept serializable initialPagePayload for server-framework bridges that cannot pass functions into client components.
  • Tracking attributes can be imported without pulling in the full React runtime.
  • Route trackers consume handoff-owned initialPageEvent: "skip" only for the first eligible mounted route, so later SPA route changes emit as browser-owned page events.

New subpath exports:

  • @contentful/optimization-react-web/handoff
  • @contentful/optimization-react-web/analytics
  • @contentful/optimization-react-web/tracking-attributes

Next.js SDK

Adds canonical Next.js handoff helpers:

  • createHandoffFromSelections
  • addBrowserHandoffMetadata
  • browser handoff metadata types
  • createNextjsEdgeOptimization
  • createEdgeRequestHandoff
  • createNextjsCacheMiddleware

createNextjsCacheMiddleware is exported from @contentful/optimization-nextjs/cache-middleware so proxy or middleware code can use it without resolving the App Router react-server entry.

Adds App Router server support for request-local handoff state so server OptimizedEntry can resolve entries from the active request or selection handoff. Analytics-only handoffs intentionally do not drive server content resolution.

App Router and Edge request handoff helpers surface the request-handoff cache-safety rejection so request-derived profile state cannot be handed to public/static cache scopes.

The App Router bound root keeps buildPagePayload as the developer-facing API and converts it to serializable initialPagePayload before crossing the Server Component to Client Component boundary.

Adds the new package subpaths:

  • @contentful/optimization-nextjs/cache-middleware
  • @contentful/optimization-nextjs/edge

Removes the previous package subpath:

  • @contentful/optimization-nextjs/esr

Node SDK

Adds:

  • createRequestHandoffFromData

This lets Node/server integrations create framework-neutral handoff payloads from completed request optimization data.

createRequestHandoffFromData accepts serialized request optimization data, optional managed entry snapshots, and cache metadata. It throws a TypeError when completed request data includes profile state and the handoff cache scope is public-permutation or static.

Rendering strategies covered

Private request handoff

For request-bound rendering where profile state may be private to the visitor.

  • cache scope: private-request
  • may include profile state
  • browser can preserve server-rendered content and hydrate SDK state
  • initial page event can be emitted by server/edge or browser depending on route ownership

Public permutation handoff

For customer-owned cacheable variants such as ISR routes keyed by segment, audience, campaign, or other non-private dimensions.

  • cache scope: public-permutation
  • requires a customer-owned cache key
  • must not include private profile state
  • supports stable cache keys and warning checks

Static handoff

For build-time or otherwise static optimized routes where selections are known without per-visitor private state.

  • cache scope: static
  • must not include private profile state
  • can preserve server-rendered content or use client-only hidden-until-ready hydration

Analytics-only handoff

For routes where optimized markup is already rendered and the browser only needs analytics, profile continuity, and interaction tracking.

  • hydration mode: analytics-only
  • uses OptimizationAnalyticsRoot or the Web SDK analytics runtime
  • does not expose content resolution APIs in the browser runtime
  • does not drive App Router server content resolution

Edge request handoff

For edge routes that resolve optimization state, emit or skip initial page events, persist anonymous IDs, and hand state to the browser.

  • new @contentful/optimization-nextjs/edge entrypoint
  • supports request/header/cookie snapshots
  • supports server consent resolution
  • preserves request context for page events
  • exposes a persist(response) helper for cookie persistence

Breaking and migration notes

/esr was replaced by /edge

@contentful/optimization-nextjs/esr has been removed.

Use:

import { createNextjsEdgeOptimization } from '@contentful/optimization-nextjs/edge'

The new edge API is the supported path for edge-owned request handoff behavior.

Handoff is explicit

Next.js integrations should pass explicit handoff payloads into the React/Web hydration layer instead of relying on implicit browser-only state for server/static/edge-rendered personalized content.

Cache ownership is explicit

Public or static rendered output must not carry private profile state. Public permutation routes should provide a customer-owned cache key. The Core SDK exposes warning helpers to make these cases visible, and request handoff helpers reject request-derived profile state when paired with public/static cache scopes.

App Router server content resolution is handoff-aware

App Router server OptimizedEntry resolves from the active request or selection handoff. This keeps the consumer API centered on the bound SDK components instead of requiring route code to manually thread selected optimizations through every server-rendered entry.

For request-personalized App Router pages, the reference implementation creates the current request handoff in a route-local server shell before returning the subtree that contains server OptimizedEntry, while keeping the persistent browser root in the shared layout.

Reference implementation updates

The App Router reference implementation demonstrates:

  • request handoff
  • customer-owned selection handoff
  • analytics-only handoff
  • hidden-until-ready hydration
  • edge request handoff
  • edge selection handoff
  • edge HTML analytics hydration
  • computed public cache metadata
  • cache middleware for public permutation routing through the dedicated middleware-safe subpath
  • handoff-owned initial page event forwarding through nested router trackers without suppressing later SPA route tracking
  • handoff-aware server OptimizedEntry rendering through the bound App Router SDK components
  • route-local request handoff creation for request-personalized server entry rendering during hard loads and client navigations

The App Router implementation separates request-owned routes from static/ISR public handoff routes with route groups, keeping the root layout request-neutral so public handoff routes prerender with revalidation. Request-owned routes keep a persistent shared browser root while route-local server shells create the current request handoff before returning server entry subtrees.

The Pages Router reference implementation aligns with the handoff contract and shared utility behavior, including handoff-owned initial page event forwarding through the nested router tracker.

The Angular Web SDK reference implementation hydrates server-provided snapshot state through hydrateOptimizationHandoff, keeping it aligned with the Web SDK handoff API instead of using lower-level snapshot hydration directly.

E2E and test coverage

Adds or updates coverage for:

  • deterministic selection fingerprints and cache keys
  • cache-safety warnings
  • framework-neutral handoff creation
  • Node request handoff creation
  • request handoff rejection for profile state in public/static cache scopes
  • Web handoff hydration
  • Web current-route initialPageEvent: "skip" deduplication for a supplied route key
  • analytics-only browser runtime behavior
  • React OptimizationRoot handoff behavior
  • React OptimizationProvider rehydration of changed handoff state after mount
  • React OptimizationAnalyticsRoot
  • React Web route tracker behavior for SSR-owned initial route skips and browser-owned SPA navigations
  • Next.js App Router handoff flow
  • App Router server OptimizedEntry resolution from request and selection handoff state
  • App Router request handoff variant rendering on /page-two hard loads and / to /page-two client navigation
  • analytics-only handoff isolation from server content resolution
  • Next.js Pages Router handoff flow
  • Next.js edge handoff behavior
  • Next.js middleware request-header forwarding
  • E2E handoff routes in the Next.js reference implementation
  • Edge HTML analytics hydration
  • reference-app router tracking with handoff-owned initial page event values
  • ISR cache-header assertions for public App Router handoff routes

Primary new E2E file:

  • lib/e2e-web/e2e/handoff.spec.ts

Documentation updates

Adds:

  • documentation/concepts/optimization-handoff-and-cache-safe-rendering.md
  • documentation/guides/rendering-personalized-nextjs-routes-with-static-isr-and-edge-handoffs.md

The rendering guide documents Edge handoff with route-handler/Response ownership and keeps App Router React page rendering in the Edge runtime out of scope until that surface is validated.

Updates:

  • Next.js App Router integration guide
  • Next.js Pages Router integration guide
  • SDK selection guide
  • analytics forwarding guide
  • React Web integration guide
  • related concept docs for state, profile sync, locale, consent, and stateless analytics
  • guide/concept indexes
  • internal SDK knowledge base
  • authoring blueprints and guide authoring checklist
  • package and implementation READMEs
  • SDK-owned request-context wording, browser-visible Next.js env var examples, and supported resolver fallback wording
  • Promise-based App Router params examples
  • public cache metadata examples to use computed tags

Bundle-size policy

This PR adds bundle budgets for new public entrypoints and removes budgets for removed entrypoints.

Notable changes include:

  • new Web SDK handoff and analytics entrypoint budgets
  • new React Web SDK handoff, analytics, and tracking-attributes entrypoint budgets
  • new Next.js cache-middleware entrypoint budgets
  • Next.js budget replacement from esr.* to edge.*
  • updated budgets for affected existing entrypoints after the handoff-capable changes
  • React Web index budgets updated for the mounted-route tracking state needed by SSR-owned initial route skips
  • App Router server bundle budgets updated to cover handoff-aware server entry resolution

Bundle budget overruns were treated as validation evidence during implementation. The budget changes reflect the current public entrypoint shape; future budget tightening should be handled as separate optimization work.

Validation

Validation evidence for this branch includes:

  • Full Next.js App Router E2E passes.
  • Full Next.js Pages Router E2E passes.
  • pnpm size:check
  • pnpm implementation:run -- nextjs-sdk_app-router build
  • pnpm --filter @contentful/optimization-nextjs typecheck
  • pnpm --filter @contentful/optimization-nextjs test:unit
  • pnpm --filter @contentful/optimization-nextjs build
  • pnpm --filter @contentful/optimization-nextjs size:check
  • pnpm --filter @contentful/optimization-react-web typecheck
  • pnpm --filter @contentful/optimization-react-web build
  • pnpm --filter @contentful/optimization-react-web size:check
  • targeted React Web provider/root unit tests for changed handoff rehydration
  • targeted Web SDK ContentfulOptimization unit tests for current-route skip behavior
  • targeted React Web route tracker unit tests for SSR-owned initial route skip and SPA navigation emission behavior
  • targeted Node request handoff unit tests after simplifying createRequestHandoffFromData
  • targeted implementation typecheck and lint for Next.js App Router and Pages Router reference implementations
  • targeted App Router handoff E2E for request handoff hard loads and client navigation
  • targeted App Router navigation E2E
  • targeted Chromium navigation E2E for both Next.js reference implementations
  • pnpm guides:check
  • pnpm knowledge:check
  • targeted Prettier checks for touched Next.js, React Web, and documentation files
  • git diff --check

Additional branch validation performed during the implementation included package, implementation, documentation, and knowledge-base checks documented in the branch work.

Reviewer checklist

Please pay particular attention to:

  • whether the cache scopes and warning behavior make the private/public/static boundary clear
  • whether initialPageEvent: "emit" | "skip" is assigned consistently across server, edge, browser ownership, and nested router trackers
  • whether handoff-owned initialPageEvent: "skip" semantics prevent duplicate browser page events for server-accepted initial routes without breaking browser-owned SPA route tracking
  • whether analytics-only handoff behavior avoids exposing content APIs while still preserving tracking needs
  • whether analytics-only handoff state is kept out of App Router server content resolution
  • whether the /edge API shape is the right replacement for the removed /esr subpath
  • whether persistent React roots rehydrate changed handoff state correctly across Pages Router SPA navigations
  • whether the App Router and Pages Router examples are realistic enough for customers to copy or adapt
  • whether App Router request-personalized pages create handoff state in the route server subtree before server OptimizedEntry rendering
  • whether React Web’s initialPagePayload bridge is clearly an adapter/runtime serialization mechanism while buildPagePayload remains the normal consumer-facing API
  • whether the Angular reference implementation uses the Web SDK handoff API appropriately
  • whether the new guide cleanly separates quick-start integration from advanced static/ISR/edge rendering guidance

Risk areas

  • The PR changes public API surfaces across Core, Web, React Web, Next.js, and Node packages.
  • The Next.js /esr subpath removal is a breaking package export change.
  • Cache-safe rendering relies on customers choosing the correct cache scope and avoiding private profile state in public/static caches.
  • Initial page event ownership is subtle because request, edge, and browser runtimes can each be responsible depending on the route.
  • App Router Server Component to Client Component boundaries require serializable props, which is why the bound Next.js root evaluates buildPagePayload on the server and passes initialPagePayload to React Web.
  • App Router request-personalized routes must create request handoff state in the route server subtree that renders server entries, not only in a shared layout that client navigation can preserve.
  • The reference implementations exercise several rendering modes, so reviewers should check both API ergonomics and route-level behavior.

@phobetron
Charles Hudson (phobetron) force-pushed the NT-3708_implement-ssg-isr-esr-handoff-support branch 4 times, most recently from 51ae699 to a5626b4 Compare July 22, 2026 09:34
@phobetron
Charles Hudson (phobetron) marked this pull request as ready for review July 22, 2026 09:35
@phobetron
Charles Hudson (phobetron) force-pushed the NT-3708_implement-ssg-isr-esr-handoff-support branch 4 times, most recently from a244b15 to fa9e129 Compare July 22, 2026 18:37
Add explicit optimization handoff primitives for rendering personalized
Contentful Optimization experiences through static, ISR, request-time, and
edge-owned routes without relying on implicit browser-only state.

Core SDK:
- Add framework-neutral handoff types for selected optimization state,
  managed entry snapshots, and cache metadata.
- Add deterministic selection fingerprints and cache-key helpers.
- Add helpers for resolving baseline entries from customer-owned selections.
- Add cache-safety warnings for public/static handoffs.

Web SDK:
- Add browser-facing content and analytics handoff types.
- Add handoff hydration for preserving server-rendered content or hiding
  client-only content until browser state is ready.
- Add an analytics-only runtime for server/static/edge-rendered markup that
  still needs browser analytics and interaction tracking.
- Treat initialPageEvent "skip" as an accepted current route so browser
  trackers do not duplicate page events already accepted by server handoffs.

React Web SDK:
- Add handoff, analytics, and tracking-attributes subpath exports.
- Teach OptimizationRoot to accept handoff state, hydration mode, route keys,
  and initial page event payload builders.
- Hydrate changed handoff state into an existing live provider runtime after
  mount so persistent roots can consume new server handoffs.
- Add OptimizationAnalyticsRoot for analytics-only hydration.
- Keep tracking attributes available independently from the full React runtime.
- Consume initialPageEvent "skip" only for the first eligible mounted route so
  later SPA navigations emit normally.

Next.js SDK:
- Add canonical handoff helpers for App Router and Pages Router integrations.
- Add the edge entrypoint with createNextjsEdgeOptimization and
  createEdgeRequestHandoff.
- Add a dedicated /cache-middleware subpath with createNextjsCacheMiddleware.
- Keep App Router factories as bound singleton-style component sets, not
  isolation boundaries.
- Clear stale App Router server content handoff state for analytics-only
  handoffs.
- Replace the previous /esr export with /edge.

Node SDK:
- Add createRequestHandoffFromData for framework-neutral request handoff
  creation from completed Node request optimization data.
- Reject public/static cache scopes when request handoff data includes profile
  state.

Reference implementations and tests:
- Expand the Next.js App Router implementation with request handoff,
  customer-owned selection handoff, analytics-only, hidden-until-ready, edge
  request, edge selection, edge HTML, and cache middleware routes.
- Compute public cache metadata from shared fixture data instead of hard-coded
  cache keys.
- Add E2E coverage for handoff routes, ISR cache headers, raw response
  metadata, and Edge HTML analytics hydration.
- Add React Web route tracker coverage for SSR-owned initial route skips and
  browser-owned SPA navigation emissions.

Documentation:
- Add a dedicated guide for rendering personalized Next.js routes with static,
  ISR, and edge handoffs.
- Fix App Router params examples for Promise-based params.
- Align guide and README cache metadata examples with computed tags.
- Clarify request-context ownership, cache scope boundaries, and supported
  rendering-mode responsibilities.

Bundle policy:
- Add budgets for new public entrypoints, including Next.js cache-middleware.
- Remove budgets for removed entrypoints.
- Reset affected budgets to match the current handoff-capable bundle outputs,
  including the React Web route tracking fix.

Validation:
- Full Next.js App Router E2E passes.
- Full Next.js Pages Router E2E passes.
- React Web focused route tracker unit tests pass.
- React Web typecheck, build, and size check pass.

BREAKING CHANGE: @contentful/optimization-nextjs/esr is removed. Use
@contentful/optimization-nextjs/edge and the new edge handoff APIs instead.

[[NT-3708](https://contentful.atlassian.net/browse/NT-3708)]
@phobetron
Charles Hudson (phobetron) force-pushed the NT-3708_implement-ssg-isr-esr-handoff-support branch from fa9e129 to 7472d08 Compare July 22, 2026 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant