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 guardsThe 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:
bindNextjsAppRouterOptimization()createRequestHandoff() through the bound OptimizationRootuse cache, cacheLife(), and cacheTag()OptimizationAnalyticsRootOptimizedEntryOptimizedEntry in Client ComponentsOptimizedEntry render-prop getMergeTagValue into shared
render optionsinitialPageEvent ownership from the handoff so the browser skips only when the server or edge
request accepted the first page eventPUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANELThis 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:
createPublicPermutationHandoff() with cache metadata from the public-permutation helperconnection() in a private slotOptimizedEntry chooses the server or client implementation from the
component boundary.nvmrc)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
getServerSidePropsruntime = 'edge' routes