feat(nextjs): add cache-safe optimization handoff support - #379
Open
Charles Hudson (phobetron) wants to merge 1 commit into
Open
feat(nextjs): add cache-safe optimization handoff support#379Charles Hudson (phobetron) wants to merge 1 commit into
Charles Hudson (phobetron) wants to merge 1 commit into
Conversation
Charles Hudson (phobetron)
force-pushed
the
NT-3708_implement-ssg-isr-esr-handoff-support
branch
4 times, most recently
from
July 22, 2026 09:34
51ae699 to
a5626b4
Compare
Charles Hudson (phobetron)
marked this pull request as ready for review
July 22, 2026 09:35
Charles Hudson (phobetron)
force-pushed
the
NT-3708_implement-ssg-isr-esr-handoff-support
branch
8 times, most recently
from
July 23, 2026 09:12
d1884f7 to
3b3463a
Compare
Charles Hudson (phobetron)
force-pushed
the
NT-3708_implement-ssg-isr-esr-handoff-support
branch
2 times, most recently
from
July 25, 2026 10:04
aaa4389 to
322e290
Compare
Wiz Scan Summary
To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio. |
Charles Hudson (phobetron)
force-pushed
the
NT-3708_implement-ssg-isr-esr-handoff-support
branch
4 times, most recently
from
July 27, 2026 11:06
a4d3aa0 to
33de5d5
Compare
Charles Hudson (phobetron)
force-pushed
the
NT-3708_implement-ssg-isr-esr-handoff-support
branch
2 times, most recently
from
July 27, 2026 13:01
7a45e70 to
a83c1ef
Compare
Charles Hudson (phobetron)
force-pushed
the
NT-3708_implement-ssg-isr-esr-handoff-support
branch
12 times, most recently
from
July 28, 2026 13:24
4d38dd6 to
f9e058e
Compare
Add explicit, cache-safe Optimization SDK handoff primitives for static, request-time, ISR, App Router Cache Components, Edge, and analytics-only rendering flows. Core SDK: - Add framework-neutral handoff state, cache metadata, cache-key helpers, deterministic selection fingerprints, cache-safety checks, and selected-entry resolution helpers. - Widen state interceptors for profileless selection state and sparse own-property patch semantics. - Restore preview selected optimizations and Custom Flag changes independently when preview overrides reset. Web SDK: - Add browser content and analytics handoff types and hydration helpers. - Add analytics-only runtime hydration. - Guard async handoff hydration against stale state publication. - Hydrate static/public profileless handoffs into live state without overwriting durable LocalStore continuity, including after delayed persistence consent. - Keep private/profile-backed handoffs on the normal durable persistence path. - Let skip-only current-page tracking mark a route accepted without requiring a page payload builder. React Web SDK: - Add handoff, analytics, and tracking-attributes entrypoints. - Teach providers and roots to hydrate handoff state, preserve server-rendered content, support analytics-only roots, and keep route tracking ownership explicit. - Preserve initial-route skip semantics through StrictMode replay while still emitting later route changes. Next.js SDK: - Add canonical App Router, Pages Router, Edge, cache-middleware, and request handoff helpers. - Add public permutation handoff/cache metadata helpers for app-owned cacheable selections. - Make App Router forwarded request-context consumption explicit with trustedRequestHandoff: true. - Forward compact trusted request context containing consent, pageAccepted, and profileId instead of full optimization data. - Preserve existing rewrite responses while applying SDK request context and eligible anonymous-ID cookie persistence. - Expire domain/path-scoped anonymous-ID cookies with matching options when persistence is denied. - Preserve terminal redirects and non-middleware app responses without SDK mutation. - Replace the previous /esr export with /edge. Node SDK: - Add createRequestHandoffFromData for framework-neutral private request handoff creation from completed server optimization data. React Native SDK: - Rename ContentfulOptimization.create() to ContentfulOptimization.initialize() and remove the beta create() API. - Preserve omitted persisted-continuity fields while allowing own-present undefined fields to clear stored continuity. Reference implementations and E2E: - Update Next.js App Router and Pages Router implementations for request, public permutation, analytics-only, static, ISR, and Edge handoff flows. - Remove duplicate selected-optimization state from the Pages Router ISR reference route and render from the handoff-backed path. - Add explicit Cache-Control: private, no-store to the private Edge request handoff route. - Add coverage for baseline empty-selection public permutations, analytics-only handoff, Pages ISR cache-control, middleware overrides, and offline queue recovery. Docs and knowledge base: - Update public guides, concepts, and SDK knowledge for cache-safe handoff behavior, continuity rules, trusted request context, cache invalidation, and analytics hydration ownership. - Document that rendered Custom Flag changes must be included in app-owned cache-version rotation for public/static handoff routes. - Document Next 16 proxy.ts/proxy naming and Next 13-15 middleware.ts/middleware naming for App Router request-context setup. - Clarify that existing rewrites are preserved while request-context SDK work is still applied. Bundle policy: - Refresh bundle budgets from full package builds and size reports. - Reset budgets across package manifests so each budget has at least 100B of gzip headroom, rounded up to the nearest 100B. - Lower loose budgets and raise tight budgets based on the current measured outputs. - Add missing budgets for real React Native preview and React Web router adapter entrypoints. - Confirm the final package graph passes size checks. Validation: - pnpm build - pnpm size:report - pnpm size:check - pnpm lint - pnpm typecheck - pnpm test:unit - pnpm knowledge:check - pnpm guides:check - pnpm format:check - pnpm build:pkgs - Targeted Web SDK, React Web SDK, Next.js SDK, Core SDK, React Native, mocks, and implementation checks - Targeted Next.js App Router, Pages Router, and Edge runtime handoff E2E - git diff --check Versioning note: - Merge this PR with this non-major squash commit message so shared package releases stay on feature-level version bumps. - Follow up with a Next.js-only release-signaling PR for the /esr replacement and create-style setup API removals, so the CD system can assign the major version bump only to @contentful/optimization-nextjs. [[NT-3708](https://contentful.atlassian.net/browse/NT-3708)]
Charles Hudson (phobetron)
force-pushed
the
NT-3708_implement-ssg-isr-esr-handoff-support
branch
from
July 28, 2026 15:17
f9e058e to
b000b65
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements explicit, cache-safe Optimization SDK handoff support for Next.js static, App Router Cache Components, Pages Router ISR, request-time, Edge runtime, and analytics-only rendering flows.
This PR adds framework-neutral handoff primitives in Core, browser handoff hydration in Web, React handoff providers and roots, canonical Next.js handoff APIs, Edge runtime helpers, a Node request handoff helper, updated reference implementations, E2E coverage, and public/internal documentation for the supported rendering modes. It also renames shared-runtime SDK setup APIs away from factory-style
create*names so API names match the single-runtime architecture.Request-derived handoffs are restricted to
private-requestcache metadata, keeping private request state out of public or static cache paths. App-owned public permutations use helper-created cache metadata instead of ad hoc cache-key construction. Web-family custom adapters can hydrate validated handoff state through the public@contentful/optimization-web/handoffsurface instead of bridge-support internals.The implementation keeps browser hydration safe across async and cached rendering paths. Web handoff hydration suppresses stale async state publication, clears stale content state when a content-capable handoff omits
changesorselectedOptimizations, marks accepted empty or undefined handoff state as successful, and keeps static/public profileless handoffs in live memory without overwriting durableLocalStorecontinuity, including after delayed persistence consent. React Native persistence follows the same field-presence model: omitted continuity fields are preserved and own-presentundefinedfields clear stored continuity.Next.js request and middleware helpers preserve framework chainability and terminal app responses. Middleware can resolve consent, perform the server page request once, persist
ctfl-opt-aid, and forward compact trusted context{ consent, pageAccepted, profileId }. Existing rewrite responses are preserved while SDK request context and eligible cookie persistence are still applied; terminal redirects and non-middleware app responses remain terminal. App RoutercreateRequestHandoff()consumes forwarded context only whentrustedRequestHandoff: trueis passed, refetches profile state fromprofileId, and uses forwardedpageAcceptedto assign first-page event ownership: accepted server tracking skips the initial browser page event, while unaccepted forwarded tracking leaves the browser to emit it. Anonymous-ID denial expires domain/path-scoped cookies with matching options.React Web validates cache safety before initial snapshot rendering can consume handoff state, keeps SSR-owned initial route skips scoped to the hydrated route including under StrictMode replay, and still emits later browser-owned route changes normally.
The docs, concepts, guides, internal SDK knowledge base, and bundle budgets describe the supported cache-safe handoff, consent, locale, profile continuity, analytics, native SDK, and cache-key behavior across Web, React Web, Next.js App Router, Next.js Pages Router, React Native, iOS, Android, and shared concepts.
Jira: NT-3708
Versioning note
Merge this PR with a non-major squash commit message so shared package releases stay on feature-level version bumps. A follow-up Next.js + React Web release-signaling PR is needed for the
/esrreplacement, create-style setup API removals, and React Web handoff prop replacement, so the CD system can assign major version bumps only to@contentful/optimization-nextjsand@contentful/optimization-react-web.Why
Optimized content rendering needs to support more than browser-owned runtime state. This PR makes state handoff explicit for:
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, and public adapter APIs from bridge-support internals.
Review map
Suggested review order:
Core model and state semantics
packages/universal/core-sdk/src/handoff.tspackages/universal/core-sdk/src/lib/interceptor/InterceptorManager.tspackages/universal/core-sdk/src/state/applyOptimizationDataToSignals.tspackages/universal/core-sdk/src/preview-support/PreviewOverrideManager.tsBrowser hydration/runtime behavior
packages/web/web-sdk/src/handoff.tspackages/web/web-sdk/src/storage/durableContinuityPersistence.tspackages/web/web-sdk/src/analytics.tspackages/web/web-sdk/src/ContentfulOptimization.tspackages/web/web-sdk/src/**React Web integration
packages/web/frameworks/react-web-sdk/src/root/OptimizationRoot.tsxpackages/web/frameworks/react-web-sdk/src/root/OptimizationAnalyticsRoot.tsxpackages/web/frameworks/react-web-sdk/src/provider/OptimizationProvider.tsxpackages/web/frameworks/react-web-sdk/src/auto-page/useAutoPageEmitter.tspackages/web/frameworks/react-web-sdk/package.jsonpackages/web/frameworks/react-web-sdk/src/**Next.js public APIs and request/edge behavior
packages/web/frameworks/nextjs-sdk/src/handoff.tspackages/web/frameworks/nextjs-sdk/src/app-router-server.tsxpackages/web/frameworks/nextjs-sdk/src/app-router-request-handoff.tspackages/web/frameworks/nextjs-sdk/src/pages-router-server.tspackages/web/frameworks/nextjs-sdk/src/pages-router.tspackages/web/frameworks/nextjs-sdk/src/edge.tspackages/web/frameworks/nextjs-sdk/src/cache-middleware.tspackages/web/frameworks/nextjs-sdk/src/request-handler.tspackages/web/frameworks/nextjs-sdk/src/server.tsxReference implementations and E2E evidence
implementations/nextjs-sdk_app-router/**implementations/nextjs-sdk_pages-router/**implementations/nextjs-sdk_app-router_edge-runtime/**implementations/web-sdk_angular/src/app/services/optimization.tslib/e2e-web/e2e/handoff.spec.tslib/e2e-web/e2e/offline-queue-recovery.spec.tslib/mocks/src/experience-handlers.tsDocumentation and SDK knowledge
documentation/concepts/optimization-handoff-and-cache-safe-rendering.mddocumentation/concepts/core-state-management.mddocumentation/concepts/consent-management-in-the-optimization-sdk-suite.mddocumentation/guides/rendering-personalized-nextjs-routes-with-static-isr-and-edge-handoffs.mddocumentation/internal/sdk-knowledge/**Public API changes
Core SDK
Adds framework-neutral handoff support:
OptimizationSelectionStateOptimizationCacheScopeOptimizationHandoffOptimizationCacheSafetyWarningcreateSelectionFingerprintcreateOptimizationCacheKeycreatePublicPermutationCacheMetadataresolveEntriesForSelectionscreateHandoffFromSelectionsgetOptimizationCacheSafetyWarningsassertOptimizationCacheSafetyLifecycleInterceptors.statenow acceptsOptimizationSelectionState, so interceptors can handle profileless selection state produced by handoffs. Sparse interceptor returns are merged after each interceptor: omitted fields preserve the prior state visible to later interceptors, while own-presentundefinedfields intentionally clear state.Web SDK
Adds browser handoff and analytics-only runtime support:
ContentOptimizationHandoffAnalyticsOptimizationHandoffBrowserOptimizationHandoffhydrateOptimizationHandoffhydrateOptimizationHandoffStateinitializeOptimizationAnalyticsRuntimehydrateOptimizationAnalyticsHandoffNew subpath exports:
@contentful/optimization-web/handoff@contentful/optimization-web/analyticshydrateOptimizationHandoffandhydrateOptimizationAnalyticsHandoffvalidateinitialPageEventand cache safety before publishing browser state. Handoff state hydration awaits Web SDK state interceptors, uses own-property field presence for patch semantics, and preserves profile continuity unless profile is present or intentionally cleared.Static and public profileless handoffs hydrate the current page's
changesandselectedOptimizationsinto live in-memory state, but they do not overwrite durableLocalStorecontinuity, including after delayed persistence consent. Content-capable handoffs that omitchangesorselectedOptimizationsclear stale content state from previous routes. Private/profile-backed handoffs keep the normal persistence path.ContentfulOptimization.trackCurrentPage({ initialPageEvent: "skip", routeKey })marks the supplied route key as accepted without emitting a browser page event and without requiring a page payload builder. 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 when a payload builder is available.React Web SDK
Adds handoff-aware provider and root support:
OptimizationProvideraccepts handoff state, hydration behavior, and managed-entry prefetch handoff entries.OptimizationProvidervalidates handoff cache safety before initial snapshot rendering can consume handoff state.OptimizationProviderhydrates changed handoff state into an existing live runtime so persistent roots can consume new server handoffs after mount.OptimizationRootcomposes provider state, live updates, route identity, and initial page event payload builders.OptimizationRootcan mark a skipped initial route accepted from a handoff androuteKeywithoutbuildPagePayloadorinitialPagePayload.OptimizationAnalyticsRootsupports analytics-only hydration and limits initial-route skips to the route hydrated from that handoff.initialPagePayloadfor server-framework bridges that cannot pass functions into client components.New subpath exports:
@contentful/optimization-react-web/handoff@contentful/optimization-react-web/analytics@contentful/optimization-react-web/tracking-attributesNext.js SDK
Adds canonical Next.js handoff and setup helpers:
createHandoffFromSelectionscreatePublicPermutationHandoffcreatePublicPermutationCacheMetadataaddBrowserHandoffMetadatabindNextjsAppRouterOptimizationbindNextjsPagesRouterOptimizationbindNextjsPagesRouterServerOptimizationconfigureNextjsServerOptimizationconfigureNextjsEdgeOptimizationcreateEdgeRequestHandoffcreateNextjsPublicPermutationCacheMiddlewarecreateNextjsOptimizationContextHandlercreateNextjsPublicPermutationCacheMiddlewareis exported from@contentful/optimization-nextjs/cache-middlewareso proxy or middleware code can use it without resolving the App Routerreact-serverentry. It consumes public-permutation cache metadata, validates Next.js cache tags, supports default query-param rewrites, passes encoded cache keys to custom rewrites, removes stale SDK-ownedx-ctfl-opt-*request overrides, and preserves existing non-SDK middleware response and request override state.createNextjsOptimizationContextHandler()can be configured withsdkandconsentto resolve consent, call the server page request once, persistctfl-opt-aidwhen allowed, and forwardx-ctfl-opt-server-dataas compact trusted context containingconsent,pageAccepted, and an optionalprofileId. When chained after an existing rewriteNextResponse, it preserves the rewrite while still applying SDK request context and eligible cookie persistence. It reads effective forwarded cookie headers for consent and profile binding.App Router
createRequestHandoff()consumes forwarded trusted request context only withtrustedRequestHandoff: true. That opt-in should be used only for routes covered by the sanitizing request handler; raw client-supplied forwarded data is ignored otherwise. Trusted forwarded context can refetch profile state server-side fromprofileId, including blocked no-data results, without a duplicatepage()call. It also carries the request handler'spageAcceptedresult so accepted server tracking skips the initial browser page event, while unaccepted forwarded tracking leaves the browser to emit it.Pages Router request handoffs include defaults derived from resolved server consent. The bound Pages Router client components prefer those handoff defaults over static
consent.clientDefaults, keeping SSR consent and hydration consent aligned.App Router, Pages Router, Node, and Edge request handoff helpers reject public/static cache metadata before request evaluation. Public or static selections should use public permutation handoffs or non-request static handoffs instead.
Anonymous-ID persistence denial expires matching path/domain-scoped cookies with
ExpiresandMax-Age=0, rather than relying on a bare cookie delete.New package subpaths:
@contentful/optimization-nextjs/cache-middleware@contentful/optimization-nextjs/edgeRemoved package subpath:
@contentful/optimization-nextjs/esrNode SDK
Adds
createRequestHandoffFromDatafor framework-neutral private request handoff creation from completed server optimization data. Request handoffs are restricted toprivate-requestcache metadata; public cache scopes should use public permutation handoffs instead.React Native SDK
Renames manual runtime setup from
ContentfulOptimization.create()toContentfulOptimization.initialize(). The betacreate()API is removed rather than preserved as a legacy alias because the React Native SDK owns one shared runtime instance per app.React Native persisted continuity now follows own-property semantics: omitted fields preserve stored profile continuity, and own-present
undefinedfields clear stored profile, selected optimizations, or Custom Flag changes.Rendering strategies covered
Private request handoff
For request-bound rendering where profile state may be private to the visitor.
private-requestPublic permutation handoff
For customer-owned cacheable variants such as static routes, App Router Cache Components routes, Pages Router ISR routes, CDN-cached routes, or Edge runtime route handlers keyed by segment, audience, campaign, market, locale, or another non-private dimension.
public-permutationStatic handoff
For build-time or otherwise static optimized routes where selections are known without per-visitor private state.
staticAnalytics-only handoff
For routes where optimized markup is already rendered and the browser only needs analytics, profile continuity, and interaction tracking.
analytics-onlyOptimizationAnalyticsRootor the Web SDK analytics runtimeEdge request handoff
For Edge runtime route handlers that resolve optimization state, emit or skip initial page events, persist anonymous IDs, and hand state to the browser.
@contentful/optimization-nextjs/edgeentrypointRequest,Headers, and cookie snapshotspersist(response)helper for cookie persistenceCache-Control: private, no-storeReference implementation updates
The App Router reference implementation uses
bindNextjsAppRouterOptimization()and demonstrates:trustedRequestHandoff: trueuse cache,cacheLife(), andcacheTag()connection()in a private slotOptimizedEntryrendering through the bound App Router SDK componentsActual Edge runtime route-handler coverage lives in the dedicated
nextjs-sdk_app-router_edge-runtimeimplementation. It usesruntime = "edge", assertsglobalThis.EdgeRuntime === "edge-runtime", avoids Node-only APIs, and demonstrates request and public permutation handoff with@contentful/optimization-nextjs/edge.The Pages Router reference implementation uses
bindNextjsPagesRouterOptimization()andbindNextjsPagesRouterServerOptimization(), and includes agetStaticProps/getStaticPathsISR public permutation route alongside the request-handoff pages. The ISR route derives selected entry rendering from the handoff-backedOptimizedEntrypath instead of carrying a second selected-optimization prop.The Next App Router and Pages Router implementation
buildscripts self-start mocks when/healthis unavailable, wait for readiness, runnext build, and stop only mock processes they started. If mocks are already running, the build treats them as externally owned and leaves cleanup to the caller orserve:stop.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.Documentation updates
Adds:
documentation/concepts/optimization-handoff-and-cache-safe-rendering.mddocumentation/guides/rendering-personalized-nextjs-routes-with-static-isr-and-edge-handoffs.mdUpdates:
The docs record that public/static handoff helpers serialize app-supplied selections and cache metadata. They do not discover public permutations or derive selected optimizations from route, cookie, header, locale, or cache-key inputs. Rendered Custom Flag
changesmust be included in app-owned cache-version rotation when those changes can affect cached output. The App Router guide documentstrustedRequestHandoff: true, compact trusted request context, forwardedpageAcceptedfirst-event ownership, the sanitizing request-handler boundary, and the Next 16proxy.ts/proxyversus Next 13-15middleware.ts/middlewarenaming split. The static/ISR guide clarifies app-owned cache inputs, Next.js invalidation tags, and SDK-generatedhandoff.cache.keymetadata. The Pages Router guide documents request-handoff consent defaults, and the static/ISR guide includes cache-control validation withs-maxage=60.Bundle-size policy
This PR adds bundle budgets for new public entrypoints, removes or replaces budgets for removed entrypoints, and resets affected package budgets after full package builds and size reports.
Budget reset policy used for this PR:
pnpm size:reportpnpm size:checkNotable budget coverage includes:
handoff,analytics,presentation, tracking-attributes, web-components, and UMD bundleshandoff,analytics,tracking-attributes, index, and router adapter entrypointsBundle budget overruns were treated as validation evidence during implementation. The final budget pass built all packages, generated size reports for all packages, reset budgets from measured outputs, and passed
pnpm size:check.E2E and test coverage
Adds or updates coverage for:
initialPageEvent: "skip"deduplication for a supplied route keyOptimizationRoothandoff and page-event wiring behavior, including skip-only route marking without a payload builderOptimizationProviderprovider-owned hydration and rehydration of changed handoff state after mountOptimizationProvidercache-safety assertion before initial snapshot runtime creationOptimizationAnalyticsRoot{ consent, pageAccepted, profileId }context, effective forwarded cookies, and App Router trusted context consumption without duplicate profile fetchespageAcceptedcontrolling trusted App Router initial browser page-event skip/emit behaviorOptimizedEntryresolution from request and public permutation handoff states-maxage=60Cache-Control: private, no-store@contentful/optimization-nextjs/edgepackage export guardPrimary E2E files:
lib/e2e-web/e2e/handoff.spec.tslib/e2e-web/e2e/offline-queue-recovery.spec.tsValidation
Validation evidence for this branch includes:
pnpm buildpnpm size:reportpnpm size:checkpnpm lintpnpm typecheckpnpm test:unitpnpm guides:checkpnpm knowledge:checkpnpm format:checkpnpm build:pkgsgit diff --checkAdditional focused validation included:
@contentful/optimization-core size:reportpnpm size:checkafter budget reset7 passed, 5 skipped3 passed, 9 skippedpnpm guides:checkandpnpm knowledge:checkReviewer checklist
Please pay particular attention to:
initialPageEvent: "emit" | "skip"is assigned consistently across server, edge, browser ownership, and nested router trackersinitialPageEvent: "skip"semantics prevent duplicate browser page events for server-accepted initial routes without breaking browser-owned SPA route trackingpageAcceptedfor first-event ownership without trusting raw client-supplied headers/edgeAPI shape is the right replacement for the removed/esrsubpathRisk areas
/esrsubpath removal changes the published Next.js package export surface.changesrequire app-owned cache-version rotation when those changes can affect cached output.trustedRequestHandoff: trueonly behind the sanitizing request handler.buildPagePayloadon the server and passesinitialPagePayloadto React Web.