Contentful Personalization & Analytics
    Preparing search index...

    Contentful Logo

    Contentful Personalization & Analytics

    Next.js SDK App Router Reference Implementation

    Readme · Guides · Reference · Contributing

    This reference implementation integrates @contentful/optimization-nextjs with the Next.js App Router. It demonstrates request-bound personalization, public permutation handoff, analytics-only rendering, Cache Components routes, and browser takeover without importing lower-level SDKs.

    For a complete integration walkthrough, see Integrate the Optimization SDK in a Next.js App Router app. Edge runtime routes live in the Next.js App Router Edge runtime reference implementation.

    • A single server binding in lib/optimization.ts.
    • A client-only binding in lib/optimization-client.ts that runs optional before-initial-page work before the request root's browser-owned page event.
    • Request-bound Server Components with browser hydration and live updates.
    • Static public permutation and analytics-only handoff.
    • App-owned and SDK-managed Contentful entry fetching.
    • Preview panel attachment behind PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL.

    proxy.ts re-exports the app's forwarding handler and declares the routes that need Optimization context:

    export { proxy } from './lib/optimization'

    export const config = {
    matcher: [
    '/',
    '/page-two',
    '/hidden-until-ready',
    '/static-shell-private-slot',
    '/selection-handoff/:path*',
    '/analytics-only/:path*',
    ],
    }

    Next.js 13 to 15 uses the same handler from middleware.ts with a middleware export. The linked guide covers forwarding and trusted handoff options.

    Keep public chrome outside Suspense, use a meaningful fallback, and call connection() only at the private boundary. Managed prefetch overlaps the entry fetch with request initialization:

    export default function Page() {
    return (
    <AppShellChrome>
    <Suspense fallback={<PersonalizedContentFallback />}>
    <PrivateRequestSlot />
    </Suspense>
    </AppShellChrome>
    )
    }

    async function PrivateRequestSlot() {
    await connection()

    return (
    <RequestOptimizationRoot prefetchManagedEntries={[entryId]}>
    <RequestOptimizedEntry entryId={entryId}>{renderEntry}</RequestOptimizedEntry>
    </RequestOptimizationRoot>
    )
    }

    Keep provider-dependent tools inside RequestOptimizationRoot.

    lib/optimization-client.ts binds a client-only beforeInitialPage callback. The maintained callback identifies only when the URL contains ?beforeInitialPage=readiness; ordinary routes do nothing. lib/optimization.ts injects that module's ClientRequestOptimizationRoot into the server request family as a Client Component reference.

    The server passes only children, defaults, handoff, and hydration through the request root. The client root derives the current App Router route and lazy page payload in the browser, so neither the callback nor a payload-builder function crosses the Server Component boundary. It makes the direct initial page attempt, marks that attempted route through the existing non-emitting initial skip path, and emits once for each later route. Do not mount a separate request page tracker in the same subtree.

    Choose entry-fetch ownership

    Use baselineEntry when the app owns Contentful queries and caching. Use entryId when the SDK binding owns the fetch, and prefetch IDs at the request root when useful:

    <RequestOptimizedEntry baselineEntry={entry}>{renderEntry}</RequestOptimizedEntry>

    <RequestOptimizationRoot prefetchManagedEntries={[entryId]}>
    <RequestOptimizedEntry entryId={entryId}>{renderEntry}</RequestOptimizedEntry>
    </RequestOptimizationRoot>

    The reference app uses appConfig.locale for SDK configuration, event context, and Contentful CDA fetches. Keep entries single-locale; don't use withAllLocales or locale=* for SDK entry resolution. See Locale handling in the Optimization SDK Suite.

    • Node.js 24.15.0, matching .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 target the shared local mock API. Replace them only when testing against live services.

    Run the development server on http://localhost:3002:

    pnpm implementation:run -- nextjs-sdk_app-router dev
    

    Run local checks from the monorepo root:

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

    Run the shared Playwright setup and suite:

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