Contentful Personalization & Analytics
    Preparing search index...

    Integrating the Optimization Next.js SDK in a Next.js App Router app

    Use this guide to render Contentful entries with personalized server first paint in a Next.js App Router app, then let the browser continue from the same Optimization handoff.

    New to personalization? Here is the whole idea in four points:

    • In Contentful you author variants of an entry and attach them to an experience - a rule that decides which visitors see which variant.
    • On each request, Contentful's Experience API looks at the request context and picks the variant for each experience. Swapping a fetched entry for its picked variant is called resolving the entry.
    • Your app hands a Contentful entry to the SDK at the point where that entry becomes output. The SDK gives back the selected variant, or the original entry when no variant applies - the baseline fallback. You can fetch the entry yourself or give the SDK your Contentful client and an entry ID; either way, the client stays yours.
    • You render the returned entry with the same application components you already use.

    That is enough to start. The guide introduces policy and optional capabilities at the point you need them.

    You will get there in two milestones:

    • Milestone 1 - Personalized first paint from one server render. The quick start below is shippable when your policy allows server personalization.
    • Milestone 2 - Browser takeover and live updates. See Browser takeover and live updates.

    This guide uses @contentful/optimization-nextjs/app-router. The adapter binds app-local configured components and handoff helpers; your app still owns Contentful fetching, consent policy, cache keys, and where personalized output is cached. If you use the Pages Router, use the Next.js Pages Router guide instead.

    This quick start assumes an App Router route already fetches a Contentful entry and renders it with your own component. The proof is one entry whose variant appears in View Source and stays stable after hydration. Consent is granted on the server and browser only to prove the wiring; replace it in Consent, identity, profile, and reset.

    1. Install the package and keep contentful app-owned.

      Copy this:

      pnpm add @contentful/optimization-nextjs contentful
      
    2. Bind one app-local Optimization module. This binding shares one configured helper set for the app; it is not a per-route or per-request isolation context. NEXT_PUBLIC_OPTIMIZATION_CLIENT_ID is reader-owned browser-visible config. The consent values below are a quick-start policy shortcut.

      Adapt this to your use case:

      // lib/optimization.ts
      import { bindNextjsAppRouterOptimization } from '@contentful/optimization-nextjs/app-router'
      import { contentfulClient } from './contentful'

      export const {
      NextAppAutoPageTracker,
      OptimizationRoot,
      OptimizedEntry,
      createRequestHandoff,
      createHandoffFromSelections,
      createPublicPermutationHandoff,
      getServerTrackingAttributes,
      resolveEntriesForSelections,
      } = bindNextjsAppRouterOptimization({
      clientId: process.env.NEXT_PUBLIC_OPTIMIZATION_CLIENT_ID!,
      environment: process.env.CONTENTFUL_ENVIRONMENT ?? 'main',
      locale: 'en-US',
      contentful: { client: contentfulClient },
      consent: {
      server: { events: true, persistence: true },
      clientDefaults: { consent: true, persistenceConsent: true },
      },
      })
    3. Forward the original request URL so the root layout can build a stable route key. Use the handler name for your Next.js version: Next.js 16 uses proxy.ts with proxy, and Next.js 13 to 15 uses middleware.ts with middleware. The body is the same. If the filename or export name does not match the Next.js version, Next.js does not run the handler and request context is not forwarded. x-ctfl-opt-request-url is an SDK-owned request-context header, so use the exact name when forwarding the URL.

      Adapt this to your use case:

      // Next.js 16: proxy.ts and export function proxy.
      // Next.js 13 to 15: middleware.ts and export function middleware.
      import { NextResponse, type NextRequest } from 'next/server'

      export function proxy(request: NextRequest) {
      const requestHeaders = new Headers(request.headers)
      requestHeaders.set('x-ctfl-opt-request-url', request.url)

      return NextResponse.next({
      request: { headers: requestHeaders },
      })
      }

      export const config = {
      matcher: ['/((?!_next/static|_next/image|favicon.ico|api).*)'],
      }
    4. Wrap the app in the bound root and pass the request handoff. ctfl-opt-aid is the SDK-owned anonymous profile cookie; your code reads it only through the helper inputs.

      Adapt this to your use case:

      // app/layout.tsx
      +import { cookies, headers } from 'next/headers'
      +import { Suspense } from 'react'
      +import { NextAppAutoPageTracker, OptimizationRoot, createRequestHandoff } from '@/lib/optimization'
      
       export default async function RootLayout({ children }: { children: React.ReactNode }) {
      +  const requestHeaders = new Headers(await headers())
      +  const requestUrl = requestHeaders.get('x-ctfl-opt-request-url') ?? 'https://example.com/'
      +  const routeKey = new URL(requestUrl).pathname
      +  const handoff = await createRequestHandoff({
      +    cache: { scope: 'private-request' },
      +    hydration: 'preserve-server',
      +    pagePayload: { properties: { path: routeKey } },
      +    request: {
      +      cookies: await cookies(),
      +      headers: requestHeaders,
      +      url: requestUrl,
      +    },
      +  })
      
         return (
           
             
      -        {children}
      +         ({ properties: { path: routeKey } })}
      +          handoff={handoff}
      +          routeKey={routeKey}
      +        >
      +          
      +            
      +          
      +          {children}
      +        
             
           
         )
       }
      
    5. Wrap the entry where it becomes output. A render prop is the function child {(entry) => ...}; it lets you render the resolved entry with your existing component. This shortcut assumes the baseline and every eligible variant use the hero content type. If a variant can use another content type, follow the skeleton-union and narrowing path in Personalizing first paint on the server.

      Adapt this to your use case:

      // app/page.tsx
      +import { OptimizedEntry } from '@/lib/optimization'
       import { Hero } from '@/components/Hero'
      
       export default async function Page() {
         const hero = await getHeroEntry({ locale: 'en-US', include: 10 })
      
         return (
      -    
      +    
      +      {(resolvedHero) => }
      +    
         )
       }
      
    6. Verify the result. In Contentful, target the experience to all visitors and give the variant a distinctive text value. Run the app, open View Source, and find that variant text in the raw HTML. Then load the page normally and confirm the same text remains after hydration.

    Table of Contents

    The sections below walk the integration in order. First, gather the few things you can only get from outside this guide:

    • A Next.js App Router app with React Server Components, React, and React DOM already working.

    • A Contentful delivery client that can fetch the baseline entries your pages render.

    • Contentful space, environment, delivery token, and one concrete locale. Fetch entries with that locale and enough include depth for linked Optimization entries and variants.

    • At least one entry with a variant attached to an experience, authored in Contentful. Without an authored variant, the integration can still run correctly while returning the baseline, so you cannot yet distinguish working personalization from a content-authoring gap. For the first personalized-content test, target all visitors so the test request or visitor matches automatically.

    • Your Optimization project values — client ID and environment, from your Optimization project settings. Find them in the Contentful web app under Apps → Installed apps → Contentful Personalization → SDK keys. The client ID and environment are safe to expose to the browser.

      The Experience and Insights API base URLs default correctly; you only set them for mocks or non-default hosts (see How the SDK fits your app).

    You do not need a setup inventory up front. Everything else — the request handler, the root, entry wrapping, consent, tracking — is introduced by the section that needs it.

    Note

    Match your app's browser environment-variable convention. Next.js exposes NEXT_PUBLIC_* values to the browser; unprefixed server values stay server-only.

    Integration category: Required for first integration

    The App Router binding centralizes SDK configuration for route code. Define it once and import the returned app-local exports everywhere else. It is not an isolation context; do not call it per route, per request, or per visitor.

    Import path Use
    @contentful/optimization-nextjs/app-router App Router binding, request handoff, public permutation handoff, and server tracking helpers
    @contentful/optimization-nextjs/cache-middleware Public-permutation proxy and middleware rewrites
    @contentful/optimization-nextjs/client Browser-only hooks and lower-level React roots
    @contentful/optimization-nextjs/edge Edge runtime request and public permutation handoff helpers
    @contentful/optimization-nextjs/request-handler Proxy or middleware request-context forwarding and trusted forwarded server context
    @contentful/optimization-nextjs/tracking-attributes Low-level data-ctfl-* attributes for analytics-only markup

    The binding config separates policy from mechanism:

    • consent.server is the app-owned server policy for the current request; configure it explicitly because App Router request handoff helpers resolve omitted request consent to false.
    • consent.clientDefaults seeds the browser SDK before a persisted or explicit browser decision is available.
    • contentful.client is your delivery client. The SDK may call it for managed entry IDs, but it does not own your CDA credentials or query policy.

    Integration category: Required for first integration

    There are two entry-source paths. Use the one that matches where your app already owns fetching.

    • Manual entry source: your Server Component fetches a baseline entry and passes it as baselineEntry to OptimizedEntry.
    • Managed entry source: the SDK receives entryId and uses the configured contentful.client. Use this when the route knows IDs and wants SDK-managed batching or cache warming.

    Follow this pattern:

    <OptimizedEntry entryId="4ib0hsHWoSOnCVdDkizE8d" entryQuery={{ locale: 'en-US', include: 10 }}>
    {(entry) => <Hero entry={entry} />}
    </OptimizedEntry>

    Keep CDA fetches single-locale. The SDK expects directly readable fields such as fields.nt_experiences and fields.nt_variants; all-locale payloads can make variant links look unresolved and fall back to baseline.

    Integration category: Common but policy-dependent

    createRequestHandoff() takes explicit request input: headers, cookies, URL, cache metadata, hydration mode, and page payload. It remains the ergonomic Server Component helper for building a browser handoff, but Server Components do not own response cookie persistence.

    Use a response-capable request handler or middleware when the route needs SDK profile-cookie persistence before Server Components render. createNextjsOptimizationContextHandler() can resolve consent, perform the server page request, persist ctfl-opt-aid when persistence is allowed, and forward compact server context as x-ctfl-opt-server-data with the value encodeURIComponent(JSON.stringify({ consent, pageAccepted, profileId })). The handler serializes pageAccepted from the server page result and profileId when one is available; it does not put the full OptimizationData payload in request headers. After Next.js applies request overrides, App Router Server Components can read that header from headers(). createRequestHandoff() consumes valid forwarded context only when you pass trustedRequestHandoff: true; raw SDK-owned forwarded headers are ignored without that explicit opt-in. Use that trusted option only for route trees behind createNextjsOptimizationContextHandler(), which clears inbound SDK-owned x-ctfl-opt-* request headers before writing forwarded server context. Valid forwarded context must include boolean pageAccepted. When it includes profileId, the helper fetches profile and selection data server-side without a second page event. Without trusted forwarded context, the helper evaluates consent.server, calls the request page event, and returns the handoff itself.

    The SDK-owned anonymous profile cookie is ctfl-opt-aid. Your app owns any consent cookie or account record that consent.server reads. Store the consent decision where both server and browser code can read it; do not use the SDK profile cookie as your consent record.

    Integration category: Required for first integration

    Server Components render personalized first paint through the bound OptimizedEntry. If no experience applies, consent is denied, the API has no variant, or a linked variant cannot be resolved, the render receives the baseline entry.

    isEmptyVariant === true marks the SDK renderer's no-content state. It differs from the fallback cases above, which render the baseline entry. In the no-content state, the bound server OptimizedEntry keeps its host and tracking attributes but does not invoke its render prop or emit app content. The standalone ServerOptimizedEntry, imported from @contentful/optimization-nextjs/server, is the lower-level renderer for server code that already has a full resolver result and static children; it applies the same empty-content rule. An absent empty-variant flag renders normally.

    A resolved selected variant can use any Contentful content type.

    A Contentful entry skeleton is a TypeScript type that names a content type ID and its fields. Use one skeleton union, S, containing every possible baseline or variant content type. A bound server OptimizedEntry with baselineEntry uses <S, M, L>, where M is the contentful.js response mode and L is the locale type. A managed entryId uses <S, L> because M is fixed to undefined. When every variant shares the baseline content type, omit the generic and let TypeScript infer that skeleton from baselineEntry.

    Follow this pattern: declare the complete skeleton union in the Server Component and narrow in the render prop, where the resolved entry becomes page markup. The guard compares the Contentful content type ID; it does not validate fields.

    import { OptimizedEntry } from '@/lib/optimization'
    import { isEntryOfContentType } from '@contentful/optimization-nextjs/api-schemas'
    import type { Entry, EntryFieldTypes, EntrySkeletonType } from 'contentful'

    type PageSkeleton = EntrySkeletonType<{ title: EntryFieldTypes.Symbol }, 'page'>
    type HeroSkeleton = EntrySkeletonType<{ headline: EntryFieldTypes.Symbol }, 'hero'>
    type CtaSkeleton = EntrySkeletonType<{ label: EntryFieldTypes.Symbol }, 'cta'>
    type AppEntrySkeleton = PageSkeleton | HeroSkeleton | CtaSkeleton
    type AppLocale = 'en-US'

    export function PersonalizedPage({ page }: { page: Entry<PageSkeleton, undefined, AppLocale> }) {
    return (
    <OptimizedEntry<AppEntrySkeleton, undefined, AppLocale> baselineEntry={page}>
    {(entry) => {
    if (isEntryOfContentType<HeroSkeleton, undefined, AppLocale>(entry, 'hero')) {
    return <h1>{entry.fields.headline}</h1>
    }
    if (isEntryOfContentType<CtaSkeleton, undefined, AppLocale>(entry, 'cta')) {
    return <button type="button">{entry.fields.label}</button>
    }
    return <h1>{entry.fields.title}</h1>
    }}
    </OptimizedEntry>
    )
    }

    The union is a compile-time model, not a runtime filter. Narrow at the renderer boundary before reading content-type-specific fields. For lower-level resolver, managed-fetch, open-ended model, and event-stream examples, see TypeScript content-model choices.

    Routes that read request headers or cookies are request-specific. Request-derived profile handoffs must use private-request cache scope and stay out of public shared caches. Use public permutation handoff for routes that should be shared.

    Integration category: Required for first integration

    The bound OptimizationProvider handles the content SDK context, handoff, hydration mode, and managed-entry prefetch for a subtree. Use the bound OptimizationRoot at the route root because it adds initial page-event wiring. Pass routeKey and buildPagePayload to OptimizationRoot when the browser should emit an initial page event from the root handoff; those props do not belong on OptimizationProvider. In request-handler-backed routes that pass trustedRequestHandoff: true, forwarded pageAccepted: true tells the handoff to skip the browser's first page event, and pageAccepted: false tells it to emit that event. In direct Server Component routes, the request helper attempts the first page event itself. Mount the separate NextAppAutoPageTracker with initialPageEvent="skip" when the server path owns the first page event; the tracker owns later route changes.

    If you pass prefetchManagedEntries without an explicit handoff, the App Router root creates baseline static handoff behavior with hydration: 'preserve-server', no selected optimizations, and initialPageEvent: 'emit'. Use that path for baseline managed-entry warming, not request-personalized state.

    For diagnostics, pass onStatesReady to the binding config. states.eventStream contains accepted events; states.blockedEventStream contains events blocked by consent or event policy.

    Integration category: Required for first integration

    The handoff controls the first browser render over already-rendered content. liveUpdates controls whether entries may re-resolve after startup when consent, identity, profile, or preview state changes.

    Use the default locked behavior for stable first paint. Turn on liveUpdates in the binding config, route, or per-entry level only when visible content should react after hydration. The preview panel can force live re-resolution for authoring even when the normal route keeps live updates off.

    For static or browser-owned routes that should hide baseline until the browser SDK is ready, pass hydration="client-only-hidden-until-ready" to the bound OptimizationRoot or OptimizationProvider, or build that mode into the handoff.

    Entry interaction tracking

    Integration category: Common but policy-dependent

    OptimizedEntry emits view, click, and hover tracking from the resolved entry by default. Configure global defaults with trackEntryInteraction in the binding config and use per-entry props for local opt-outs. Interaction delivery still depends on event consent and profile continuity.

    Analytics-only server/static/edge markup should use getServerTrackingAttributes() so the browser analytics runtime observes the same data-ctfl-* contract without resolving content.

    Integration category: Common but policy-dependent

    Replace the quick-start consent shortcut with your app policy:

    1. Read the app-owned consent record in consent.server; omitted request consent resolves to false.
    2. Seed conservative browser defaults through consent.clientDefaults.
    3. Mirror browser choices to the app-owned consent record before the next request.
    4. Use setConsent, identifyUser, and resetUser from /client hooks for browser actions.

    Adapt this to your use case:

    bindNextjsAppRouterOptimization({
    clientId: process.env.NEXT_PUBLIC_OPTIMIZATION_CLIENT_ID!,
    environment: process.env.CONTENTFUL_ENVIRONMENT ?? 'main',
    consent: {
    server: ({ cookies }) =>
    cookies.get('app-consent')?.value === 'accepted'
    ? { events: true, persistence: true }
    : false,
    clientDefaults: { consent: false, persistenceConsent: false },
    },
    })

    app-consent is reader-owned in this example. The SDK reads only the decision you pass to it.

    Integration category: Optional

    Forward accepted events from states.eventStream after onStatesReady runs. Deduplicate by messageId, keep vendor consent separate from Contentful event consent, and use states.blockedEventStream for diagnostics instead of replay.

    The runtime event stream remains model-agnostic because it can carry interactions for entries of every content type. If you read event.optimization?.resolvedEntry, narrow that entry with isEntryOfContentType at the point of use; resolver-specific S types do not flow into a later event.

    For the full pattern, use Forwarding Optimization SDK context to analytics and tag-management tools.

    Integration category: Optional

    The OptimizedEntry render prop also receives getMergeTagValue. Pass it to your Rich Text renderer when entries contain SDK-owned merge-tag entries. Use /client hooks for browser-only Custom Flags when a route needs reactive flag reads after hydration.

    Follow this pattern:

    <OptimizedEntry baselineEntry={article}>
    {(entry, { getMergeTagValue }) => (
    <RichText document={entry.fields.body} getMergeTagValue={getMergeTagValue} />
    )}
    </OptimizedEntry>

    Integration category: Optional

    Attach @contentful/optimization-web-preview-panel only in development, preview, or staging environments. The panel needs the live browser SDK and a Contentful client or pre-fetched audience and experience entries. Keep the environment gate app-owned; do not ship editor tooling to ordinary production visitors.

    Integration category: Advanced or production-only

    Choose one ownership model per route:

    Route strategy First paint owner Browser content behavior Cache scope
    Request handoff Server request Preserves server output; optional live updates private-request
    Public permutation handoff Static generation, Cache Components, or Edge runtime route chosen by app code Preserves selected output; optional live updates public-permutation with SDK-built key
    Analytics-only handoff Server, static, or Edge runtime markup Tracks page and interactions only; no content re-resolution Matches the rendered markup owner
    Client-only hidden-until-ready Browser SDK Hides baseline until ready or timeout Static page shell

    For a static shell with a request-personalized section, keep the shell free of cookies(), headers(), and request handoff calls. In a Next.js app that uses Cache Components, put the revalidation policy in the cached component with use cache, cacheLife(), and cacheTag(). Then place the private section under Suspense and call connection() inside that private slot before reading request data. The slot must use private-request cache scope because it renders for one visitor.

    Adapt this to your use case:

    // next.config.ts
    import type { NextConfig } from 'next'

    const nextConfig: NextConfig = {
    cacheComponents: true,
    }

    export default nextConfig

    Follow this pattern:

    // app/static-shell-private-slot/page.tsx
    import { cacheLife, cacheTag } from 'next/cache'
    import { Suspense } from 'react'
    import { PrivateRequestSlot } from './PrivateRequestSlot'

    async function CachedMarketingShell() {
    'use cache'
    cacheLife('minutes')
    cacheTag('static-marketing-shell')

    return <StaticMarketingShell />
    }

    export default function Page() {
    return (
    <main>
    <CachedMarketingShell />
    <Suspense fallback={<section aria-busy="true" />}>
    <PrivateRequestSlot />
    </Suspense>
    </main>
    )
    }

    Follow this pattern:

    // app/static-shell-private-slot/PrivateRequestSlot.tsx
    import { NextAppAutoPageTracker, OptimizationRoot, createRequestHandoff } from '@/lib/optimization'
    import { cookies, headers } from 'next/headers'
    import { connection } from 'next/server'
    import { Suspense } from 'react'

    export async function PrivateRequestSlot() {
    await connection()

    const requestHeaders = new Headers(await headers())
    const requestUrl =
    requestHeaders.get('x-ctfl-opt-request-url') ?? 'https://example.com/static-shell-private-slot'
    const routeKey = new URL(requestUrl).pathname
    const handoff = await createRequestHandoff({
    cache: { scope: 'private-request' },
    hydration: 'preserve-server',
    pagePayload: { properties: { path: routeKey } },
    request: {
    cookies: await cookies(),
    headers: requestHeaders,
    url: requestUrl,
    },
    })

    return (
    <OptimizationRoot
    buildPagePayload={() => ({ properties: { path: routeKey } })}
    handoff={handoff}
    routeKey={routeKey}
    >
    <Suspense>
    <NextAppAutoPageTracker initialPageEvent="skip" />
    </Suspense>
    <PersonalizedPrivateContent />
    </OptimizationRoot>
    )
    }

    StaticMarketingShell and PersonalizedPrivateContent are app-owned components in this pattern. Cache Components do not use route-level export const revalidate; put ISR-style revalidation on the cached component or data function instead.

    If your app is not using Cache Components or partial pre-rendering, keep the route as a static shell and mount a client-owned slot that fetches a private no-store route handler or API route. That private endpoint creates the private-request handoff and returns only the data the slot needs. This fallback keeps the shell public-cacheable but moves the personalized content to the browser after the private fetch completes.

    For complete SSG, App Router Cache Components, Pages Router ISR, Edge runtime, and analytics-only recipes, use Rendering personalized Next.js routes with static, ISR, and edge handoffs. For the mechanics behind handoff state and cache scopes, use Optimization handoff and cache-safe rendering.

    Integration category: Advanced or production-only

    Use lower-level subpaths only when the bound App Router module cannot express the route. The main escape hatches are:

    • /server for direct Node request control with configureNextjsServerOptimization(...). That helper configures a stateless server runtime; it is not a request-isolation context.
    • /client for router-neutral React roots, providers, and hooks.
    • /tracking-attributes for manually rendered analytics-only markup.
    • /edge for Edge runtime route handlers that export runtime = 'edge' and avoid Node-only APIs.

    Manual flows still pass handoff to a React root. Do not invent a second state shape for browser hydration.

    Lower-level resolver calls keep selections as the optional second positional argument: resolveOptimizedEntry(entry, selectedOptimizations). Managed fetch calls use fetchOptimizedEntry(entryId, options), with selections in FetchOptimizedEntryOptions; neither call receives a content-type argument. ServerOptimizedEntry<TElement, S, M, L> places the element type first, followed by the complete skeleton union, response mode, and locale.

    When lower-level code renders a resolver result directly, isEmptyVariant === true marks the SDK renderer's no-content state; check it before rendering entry. The result retains the baseline entry and selection context for tracking even when consumer output is empty.

    Integration category: Advanced or production-only

    private-request handoffs include request-specific state and must not be stored in a shared public cache. public-permutation handoffs are for app-owned segments, campaigns, markets, or other application-defined permutations; pass permutationKey, cacheVersion, locale, entry IDs, selected optimizations, and any rendered Custom Flag changes to createPublicPermutationHandoff() so the SDK can create the public cache metadata and hydrate the same state. The helper serializes those application-supplied values; it does not discover public permutations or derive selected optimizations from route, cookie, header, locale, or cache-key inputs. Because changes are handoff state rather than part of the generated cache-key fingerprint, rotate cacheVersion when rendered Custom Flag changes change. static handoffs are for baseline or build-time output that does not depend on a request profile. Do not create public or static handoffs from request-derived profile state.

    Use the supplemental rendering guide for static generation, App Router Cache Components, Pages Router ISR, Edge runtime, and analytics-only recipes. Use the handoff concept when reviewing whether a route can be public, public-permutation, static, or private-request cached.

    Integration category: Advanced or production-only

    When no Optimization event may emit before explicit consent, configure a strict event policy and return false from consent.server until your app-owned consent record is accepted. Use initialPageEvent="skip" only when a server or edge helper already accepted the same route's first page event. Use blocked-event diagnostics to verify denied events are dropped at the SDK boundary.

    • Confirm server and browser config use the intended Contentful space, environment, locale, and Optimization client ID.
    • Confirm consent.server, browser consent defaults, and app-owned consent storage agree.
    • Confirm ctfl-opt-aid is browser-readable where server and browser profile continuity is needed.
    • Confirm server page events are not duplicated by browser route trackers.
    • Confirm baseline fallback is acceptable when no variant applies or Contentful links are unresolved.
    • Confirm request-personalized output is never stored in a public shared cache.
    • Run the maintained reference implementation or your app's equivalent typecheck, lint, build, and browser E2E checks.
    Symptom Likely cause Check
    Entries stay on baseline No matching variant, denied consent, unresolved variant links, or all-locale CDA payload Target all visitors for the first test, read accepted or blocked events, and fetch one locale with enough include depth
    A heterogeneous render cannot read content-type-specific fields The skeleton union omits a possible content type, or the entry was not narrowed before rendering Include every baseline and variant skeleton in S, then narrow with isEntryOfContentType
    Variant appears in the browser but not View Source The route is browser-owned rather than server-handoff-owned Verify the route calls createRequestHandoff() or uses a public permutation handoff before rendering
    Root layout sees no forwarded request context The handler filename or export name does not match the Next.js version, so Next.js silently skips it Use proxy.ts with proxy on Next.js 16, or middleware.ts with middleware on Next.js 13 to 15
    Duplicate first page events Both the handoff root and route tracker emitted the initial route Use the handoff's initialPageEvent for the root and set the separate tracker to skip the initial event when the server accepted it
    Live entries do not change after identify or reset The entry is locked to the handoff and live updates are off Enable live updates for the route or entry, or open the preview panel in an allowed environment
    Personalized HTML is cached for the wrong visitor Request handoff output entered a public cache Use private-request for request state and public permutation handoffs only for app-owned selected permutations