Use this document to understand how server, static, and edge-rendered Optimization state reaches the browser without putting visitor-specific profile state into public caches. It applies to the Next.js SDK, React Web SDK, and Web SDK surfaces that consume an Optimization handoff.
For setup steps, use the relevant Next.js integration guide. This concept explains the mechanics behind those guides: what a handoff contains, who owns cacheable permutations, how hydration differs from live updates, and how analytics-only markup can still carry Optimization tracking metadata.
| Runtime surface | Handoff role |
|---|---|
@contentful/optimization-nextjs/app-router |
Binds request handoff helpers, public permutation handoff helpers, roots, analytics roots, and server tracking attributes for App Router routes. |
@contentful/optimization-nextjs/pages-router and /pages-router/server |
Binds Pages Router roots, getServerSideProps request handoff helpers, and public permutation handoff helpers. |
@contentful/optimization-nextjs/edge |
Configures Edge runtime request handoff and public permutation handoff helpers. |
@contentful/optimization-nextjs/request-handler |
Forwards sanitized request context through pass-through responses and can perform response-capable server page work before App Router Server Components render. |
@contentful/optimization-nextjs/cache-middleware |
Rewrites pass-through Next.js proxy or middleware requests to the public permutation cache key produced by the same metadata helper used by handoffs. |
@contentful/optimization-react-web |
Consumes content handoffs in OptimizationRoot and analytics-only handoffs in OptimizationAnalyticsRoot. |
@contentful/optimization-web |
Hydrates content handoffs into a live browser SDK and analytics-only handoffs into a narrow analytics runtime. |
An OptimizationHandoff is the framework-neutral shape for state produced before browser hydration.
It can contain:
state.selectedOptimizations - the selected experience and variant records used for entry
resolution.state.changes - Custom Flag changes derived from the selected optimizations.state.profile - profile state from a request-backed Experience API response.entries - managed-entry baseline snapshots that let browser-managed entries hydrate from the
same baseline entry the server or static render used.cache - metadata that describes where the rendered output is allowed to be cached.Browser handoffs add two fields:
hydration - the browser presentation policy for already-rendered content.initialPageEvent - whether the browser emits or skips the first page event for this route.The SDK serializes and hydrates the state it receives. Browser hydration applies only state fields
that are present on the handoff. During Web handoff state interception, omitted interceptor fields
keep the incoming handoff value, while an own field whose value is undefined is applied
intentionally. Hydrating an undefined or empty handoff state still marks the browser Experience
request state as successful. For content handoffs, that successful empty hydration clears stale
selected optimizations and changes while preserving the existing profile unless the handoff includes
its own profile field.
The SDK does not infer application segments, campaigns, markets, or other public permutations from the URL. When a handoff is cacheable across visitors, application code supplies the selected optimizations and cache key.
Handoff is a render boundary. A server, build, Pages Router ISR, App Router Cache Components, or Edge runtime path resolves content using a known Optimization state, then passes that same state to the browser so the first browser render matches the markup.
request, build, ISR, Cache Components, or Edge runtime code chooses Optimization state
-> route resolves entries or renders analytics-only markup
-> route creates a browser handoff with cache metadata
-> browser root hydrates from the handoff
-> browser SDK takes over according to hydration and liveUpdates policy
The handoff is not a cache key by itself. Cache safety comes from matching the rendered output, the handoff state, and the cache scope.
In App Router, managed-entry prefetch without a supplied handoff creates a baseline static handoff
with hydration: 'preserve-server', selectedOptimizations: [], and
initialPageEvent: 'emit'. Treat it as baseline entry warming, not request-personalized state.
| Scope | Use it for | Cache rule |
|---|---|---|
private-request |
A real request backed by cookies, request headers, profile continuity, or an Experience API response for one visitor. | Keep the rendered output out of shared public caches. |
public-permutation |
A customer-owned segment, market, campaign, or path where application code already chose the selected optimizations. | Provide a helper-built cache.key that covers the full public permutation. |
static |
Build-time or baseline output that does not depend on request profile state. | Do not include profile state. |
getOptimizationCacheSafetyWarnings() reports diagnostics without blocking rendering.
assertOptimizationCacheSafety() throws a TypeError for the same unsafe states, and SDK handoff
constructors call it for created handoffs. Browser content and analytics handoff hydration also call
it before writing handoff state into the live Web SDK. Request handoff helpers are stricter before
request evaluation: they accept private-request cache metadata and reject public-permutation or
static cache metadata with a TypeError. Public permutation metadata requires a non-empty key at
the type level and at runtime.
React Web validates content handoff cache safety before children read the initial snapshot runtime. An unsafe public or static handoff with profile state fails before the first handoff-backed render.
A customer-owned public permutation is a cacheable output your application can name from a finite app-owned or customer-owned registry without reading a visitor profile. The registry can come from a segment service, CMS or config mapping, static artifact, or reviewed app config.
The public/static handoff helpers serialize the selected optimizations, changes, entries, and cache metadata your application supplies. They do not call the Experience API or derive selections from route, cookie, header, locale, or cache-key inputs.
A registry record is valid for a static, ISR-style, or Edge runtime public output only when it has
enough dimensions to fetch, resolve, hand off, and cache one output: public key or slug, locale,
baseline entry IDs, selectedOptimizations, optional rendered changes, and an app-owned revision
or cache version. The app-owned revision must change when selected optimizations, rendered Custom
Flag changes, rendered entries, locale, content environment, or cache policy change. The SDK
serializes and hydrates the state you supply. It does not discover public permutations for the
application.
The validity boundary is cache ownership. Public and static handoffs must use app-owned selections,
not request profile, cookies, headers, or request-derived selection data. Public cache keys come
from createPublicPermutationCacheMetadata() or a framework helper that calls it. The helper
encodes key fields such as permutation=... and version=..., then includes scope, locale,
baseline entry IDs, and selected optimizations in the suffix. cacheVersion is optional in the API,
but cacheable public routes should supply it so the app has an explicit invalidation dimension. The
generated cache.key does not fingerprint Custom Flag changes; if rendered flag values affect
the output, represent that dimension through cacheVersion or another caller-owned key. The
generated key is deterministic SDK identity and transport metadata; it is not a Next.js use cache
key and is not a cacheTag() or revalidateTag() tag.
Next.js public permutation middleware consumes the same public metadata object. Its default rewrite
uses the SDK-owned ctfl-opt-cache-key query parameter, and custom rewrites receive an
encodedCacheKey for locations that need an already-encoded value. Next.js tags are caller-owned
invalidation labels. When supplied to the Next.js helpers or middleware metadata, use no more than
128 tags; each tag must be a non-empty string after trimming, 256 characters or fewer, and must not
include commas. App Router Cache Components can pass short custom tags to cacheTag(). Pages Router
ISR and Edge runtime public routes can omit tags unless the app wires tag invalidation.
For public permutation middleware, existing middleware or proxy rewrites, redirects, or other
terminal responses are returned unchanged. The request-context handler is different: it preserves
an existing rewrite response while still applying SDK request context and eligible profile-cookie
persistence. Pass-through responses keep flowing through the Optimization rewrite or
request-context path.
For selected-optimization shape, content model, variant-index, and fallback details, see Entry optimization and variant resolution. For the procedural Next.js recipe, see Rendering personalized Next.js routes with static, ISR, and edge handoffs.
hydration controls the first browser presentation over already-rendered markup.
| Hydration mode | Effect |
|---|---|
preserve-server |
Keep server, static, ISR-style, or Edge runtime-rendered content visible while the browser hydrates; resolved metadata waits for settled state. |
client-only-hidden-until-ready |
Let the browser own content resolution; the default loading presentation can hide the baseline layout target until resolution settles or times out. |
analytics-only |
Hydrate analytics state without providing content resolution context. |
liveUpdates controls later browser re-resolution after startup. A route can preserve the rendered
content for stable first paint and still keep live updates off. Turn live updates on when visible
content must react to consent, identity, profile, or preview changes after hydration. Preview state
can force live re-resolution for authoring flows.
When Web or React Web hydrates a profileless static or public-permutation content or analytics
handoff, the handoff state can affect live browser memory for that page, but the SDK preserves
existing durable browser profile continuity by suppressing durable continuity persistence for that
handoff. A private-request handoff, or any profile-backed handoff that passes cache safety,
follows normal persistence behavior when persistence consent allows.
The first page event must have one owner.
initialPageEvent: 'skip' when a request or edge helper already accepted the first page
event for the same route.initialPageEvent: 'emit' when the browser owns the first page event for a static,
public-permutation, or browser-owned route.Next.js request helpers set this value from the accepted page event result. App Router routes that
use a response-capable request handler can forward pageAccepted to createRequestHandoff() so the
Server Component path does not call page() a second time. That forwarded context is compact:
consent, pageAccepted, and optional profileId. A trusted App Router request handoff refetches
profile and selection state server-side when profileId is present instead of forwarding full
OptimizationData. Selection handoff helpers require application code to supply the initial
page-event owner because customer-owned static and public permutations do not emit a server request
event by themselves.
React Web roots can emit the handoff-owned initial page event when they receive routeKey and
either buildPagePayload or initialPagePayload. A skip can mark the initial route accepted with
only the route key. A skip applies only to the first route hydrated from that handoff; later
route-key changes emit browser page events. Next.js route trackers use the same "emit" or
"skip" control for the first browser route and then track later navigations.
Some server, static, ISR-style, or Edge runtime routes render the final HTML themselves and use the browser SDK only for page and interaction tracking. Those routes use an analytics-only handoff:
OptimizationAnalyticsRoot hydrates an analytics runtime, not a content resolution provider.getServerTrackingAttributes(baselineEntry, resolvedData) attaches the SDK-owned data-ctfl-*
attributes that browser entry-interaction tracking consumes.data-ctfl-* attributes describe the resolved entry, baseline entry, optimization context,
variant index, sticky selection, and clickable state.When an analytics-only handoff skips the initial route, React StrictMode effect replay does not turn that skip into a duplicate browser page event. Later route-key changes still emit route events through the analytics runtime. If a newer analytics hydration starts or the root unmounts before async hydration finishes, the stale hydration stops before writing state, warning, or tracking the page.
Analytics-only rendering still needs the same cache decision as the markup it tracks. A static analytics handoff is static; a public permutation needs an application-owned key; request-personalized markup remains private to the request.
Profile state is visitor-specific. Request-backed selected optimizations, Custom Flag changes, merge tag values, and rendered personalized HTML can all depend on that profile. If that state enters a shared public cache, another visitor can receive the wrong variant, wrong Custom Flag state, wrong merge-tag output, or a page-event handoff that was created for a different profile.
Use request-backed handoffs for private request rendering. Use public permutation handoffs for cacheable app-owned permutations. Cache raw Contentful baseline entries according to your application policy, but keep resolved personalized output scoped to the state that produced it.