Contentful Personalization & Analytics
    Preparing search index...

    Contentful Logo

    Contentful Personalization & Analytics

    Next.js SDK App Router Reference Implementation

    Readme · Guides · Reference · Contributing

    Reference implementation for @contentful/optimization-nextjs in a Next.js App Router application with bound server/client components, explicit Optimization handoff, analytics-only handoff, public permutation handoff, and Cache Components routes. The implementation binds OptimizationRoot, OptimizationAnalyticsRoot, OptimizedEntry, NextAppAutoPageTracker, request handoff helpers, and public permutation handoff helpers once in @/lib/optimization with bindNextjsAppRouterOptimization(). Routes and shared components import those app-local exports for Server Component first paint, Client Component live-update surfaces, Cache Components public permutation handoff, and analytics-only routes. Other SDK runtime imports use Next.js SDK package subpaths. The package root is not imported:

    • @contentful/optimization-nextjs/app-router in @/lib/optimization for the bound component binding, route tracker, request handoff, public permutation handoff, cache middleware, and tracking helpers
    • @contentful/optimization-nextjs/client for browser hooks and providers
    • @contentful/optimization-nextjs/api-schemas in components that need SDK schema guards

    The Next.js SDK adapter delegates server and browser SDK work internally, so this implementation does not import, configure, or externalize lower-level SDK packages directly for Optimization runtime work. Actual Edge runtime routes live in the Next.js App Router Edge runtime reference implementation.

    Use this implementation when you need a Next.js example where Server Components fetch Contentful entries, the bound server root prepares Optimization state for handoff, and the browser SDK resolves live surfaces after startup. It covers:

    • App-local bound components from bindNextjsAppRouterOptimization()
    • Request handoff from createRequestHandoff() through the bound OptimizationRoot
    • Customer-owned public permutation handoff with helper-created public cache metadata
    • Cache Components SSG and ISR-style revalidation with use cache, cacheLife(), and cacheTag()
    • Analytics-only tracking over server-rendered markup through OptimizationAnalyticsRoot
    • Client-only hidden-until-ready hydration for static or browser-owned routes
    • Server-resolved first paint and static content with bound OptimizedEntry
    • Browser-side entry resolution with the same app-local OptimizedEntry in Client Components
    • Rich Text merge tags passed from the OptimizedEntry render-prop getMergeTagValue into shared render options
    • Live re-resolution after consent, identify, reset, and client-side route changes
    • initialPageEvent ownership from the handoff so the browser skips only when the server or edge request accepted the first page event
    • Preview panel attachment behind PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL

    This App Router pattern keeps server fetching in place, passes explicit handoff state to the browser, uses Server Components for first paint and static content, and uses Client Components for live-update surfaces that need browser takeover.

    First request
      proxy.ts
        re-exports proxy from lib/optimization.ts and declares the literal Next.js matcher config
          forwards trusted private request handoff data through the request handler
          keeps public permutation routes on the cache middleware rewrite path
    
      lib/optimization.ts
        bindNextjsAppRouterOptimization()
          exports OptimizationRoot, OptimizationAnalyticsRoot, OptimizedEntry, NextAppAutoPageTracker,
          createRequestHandoff, createPublicPermutationHandoff, createOptimizationCacheKey, tracking
          helpers, and selection resolution helpers
    
      app/(request)/layout.tsx
        renders the persistent bound OptimizationRoot, preview panel, route tracker, and request route
        shell from the cached current request handoff
    
      lib/request-handoff.ts
        creates the cached current request handoff from explicit headers/cookies/url request input
    
      app/(request)/RequestRouteShell.tsx
        creates the current request handoff before returning the route subtree with server entries
    
      app/(request)/page.tsx and app/(request)/page-two/page.tsx
        fetch CDA entries server-side
        render RequestRouteShell before server first-paint entries through the bound OptimizedEntry
    
      app/(static)/selection-handoff/[segment]/layout.tsx and page.tsx
        render the customer-owned public permutation selected by the static layout handoff with
        helper-created public cache metadata and Cache Components revalidation
    
      app/(static)/analytics-only/[segment]/layout.tsx and page.tsx
        render selected content plus data-ctfl-* attributes for the layout analytics root with the same
        public cache metadata
    
      app/(static)/static-shell-private-slot/page.tsx and PrivateRequestSlot.tsx
        render a static shell with request-personalized content isolated in a private slot
    
      app/layout.tsx
        stays request-neutral so Cache Components route groups can pre-render
    
    Browser runtime
      Bound OptimizationRoot hydrates explicit handoff state
      OptimizationAnalyticsRoot hydrates analytics-only handoff without content re-resolution
      NextAppAutoPageTracker emits route page events
      The same app-local OptimizedEntry resolves entries from current selectedOptimizations
      LiveUpdatesProvider controls reactive re-resolution
    

    The implementation defines one locale at appConfig.locale, passes it to the Next.js SDK server helpers, uses it for event context, and passes it directly to Contentful CDA fetches. Browser client resolution reuses the single-locale entries supplied by the server. Do not use contentful.js withAllLocales or raw CDA locale=*; SDK entry resolution expects direct single-locale fields such as fields.nt_experiences and fields.nt_variants.

    See Locale handling in the Optimization SDK Suite and Entry personalization and variant resolution.

    Use Server Components for routes that fetch Contentful entries and render first-paint/static content through the bound OptimizedEntry. Use Client Components for entry surfaces that resolve and react after browser startup. Pass request, selection, or analytics-only handoff to the bound root instead of sharing package-internal state objects. This implementation covers:

    • The home route fetches entries server-side, renders static first-paint entries on the server, and keeps merge-tag and live-update examples on the client
    • The page-two route covers client navigation and browser-observable page events
    • The selection-handoff route renders a customer-owned segment through createPublicPermutationHandoff() with cache metadata from the public-permutation helper
    • The analytics-only route renders selected markup with tracking attributes and no browser content re-resolution
    • The hidden-until-ready route covers client-only hidden-until-ready hydration
    • The static-shell-private-slot route keeps the shell static and isolates request-personalized content under connection() in a private slot
    • The same app-local bound OptimizedEntry chooses the server or client implementation from the component boundary
    • Node.js >= 20.19.0 (24.15.0 recommended to match .nvmrc)
    • pnpm

    Run these commands from the monorepo root:

    pnpm install
    pnpm build:pkgs
    pnpm implementation:run -- nextjs-sdk_app-router implementation:install
    test -f implementations/nextjs-sdk_app-router/.env || cp implementations/nextjs-sdk_app-router/.env.example implementations/nextjs-sdk_app-router/.env

    The .env.example values are mock-safe defaults for the shared local mock API. Provide live Contentful and Optimization values only when testing against real services. PUBLIC_OPTIMIZATION_STATIC_HANDOFF_MISSING_ENTRY controls static public handoff routes when a required Contentful entry is missing: keep the default throw to fail the route, or use not-found when you intentionally want those routes to return a 404.

    Run these commands from the monorepo root:

    pnpm implementation:run -- nextjs-sdk_app-router dev
    pnpm implementation:run -- nextjs-sdk_app-router build
    pnpm implementation:run -- nextjs-sdk_app-router typecheck
    pnpm implementation:run -- nextjs-sdk_app-router lint

    The development server runs on http://localhost:3002.

    For production-style local serving with PM2-managed mock and app processes:

    pnpm implementation:run -- nextjs-sdk_app-router serve
    pnpm implementation:run -- nextjs-sdk_app-router serve:stop

    The preview panel attaches when PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL is true. The default .env.example keeps it disabled for mock-safe local runs; enable the flag only for development, preview, or staging builds where editor tooling is intended.

    Run the full E2E setup and test suite from the monorepo root:

    pnpm setup:e2e:nextjs-sdk_app-router
    pnpm test:e2e:nextjs-sdk_app-router

    The E2E suite reuses the shared lib/e2e-web browser scenarios for CSR and hydration behavior under the App Router Cache Components configuration. It covers shared variant resolution, tracking, navigation, live updates, offline queue recovery, and the hydration check that a consented server handoff does not issue a duplicate client Experience request. JavaScript-disabled SSR checks are skipped because Cache Components reveal streamed request-personalized content with Next.js runtime scripts.

    Use Playwright UI or codegen when needed:

    pnpm implementation:run -- nextjs-sdk_app-router test:e2e:ui
    pnpm implementation:run -- nextjs-sdk_app-router test:e2e:codegen