Warning
The Optimization SDK Suite is pre-release (alpha). Breaking changes may be published at any time.
React Web SDK package for @contentful/optimization-react-web.
Core root/provider primitives and React-facing APIs are implemented.
OptimizationProvider+useOptimization()context behavioruseOptimizationContext()readiness/error accessLiveUpdatesProvider+useLiveUpdates()global live updates contextOptimizationRootprovider composition and defaultsuseOptimizedEntry()imperative optimization resolutionOptimizedEntryentry resolution, lock/live-update behavior, loading fallback, and data-attribute mapping
@contentful/optimization-react-web is intended to become the React framework layer on top of
@contentful/optimization-web.
From repository root:
pnpm --filter @contentful/optimization-react-web build
pnpm --filter @contentful/optimization-react-web typecheck
pnpm --filter @contentful/optimization-react-web test:unit
pnpm --filter @contentful/optimization-react-web devFrom this package directory:
pnpm build
pnpm typecheck
pnpm test:unit
pnpm dev- package metadata and dual module exports
rslib/rsbuild/rstest/TypeScript baseline aligned with Web SDK patterns- core provider/root/context primitives in
src/ OptimizedEntrycomponent with loading-state support and Web SDK data-attribute tracking- scaffold dev dashboard harness with the host shell in
dev/and the React app indev/app/for consent, identify/reset, state, events, and entries
Pass configuration props directly to OptimizationRoot (recommended) or OptimizationProvider. The
SDK is initialized internally by the provider. OptimizationProvider can also receive a prebuilt
sdk instance when ownership needs to stay outside React.
import { OptimizationRoot } from '@contentful/optimization-react-web'
function App() {
return (
<OptimizationRoot
clientId="your-client-id"
environment="main"
api={{
insightsBaseUrl: 'https://ingest.insights.ninetailed.co/',
experienceBaseUrl: 'https://experience.ninetailed.co/',
}}
liveUpdates={true}
>
<YourApp />
</OptimizationRoot>
)
}Available config props:
| Prop | Type | Required | Description |
|---|---|---|---|
clientId |
string |
Yes | Your Contentful Optimization client identifier |
environment |
string |
No | Contentful environment (defaults to 'main') |
api |
CoreApiConfig |
No | Unified Experience API and Insights API configuration |
app |
App |
No | Application metadata for events |
autoTrackEntryInteraction |
AutoTrackEntryInteractionOptions |
No | Automatic entry interaction tracking options |
logLevel |
LogLevels |
No | Minimum log level for console output |
liveUpdates |
boolean |
No | Enable global live updates (defaults to false) |
OptimizationRoot composition order:
OptimizationProvider(outermost)LiveUpdatesProvider- application children
useOptimization()returns the initializedContentfulOptimizationinstance.useOptimizationContext()returns{ sdk, isReady, error }without requiring readiness.useOptimizedEntry({ baselineEntry, liveUpdates })returns resolved entry data and optimization state for imperative consumers.useOptimization()throws if used outsideOptimizationProvider.useOptimization()also throws if the provider exists but the SDK is not ready.useLiveUpdates()throws if used outsideLiveUpdatesProvider.
Router adapters are published as isolated subpath exports so applications can import only the router they use.
The Next.js Pages Router adapter:
import type { AppProps } from 'next/app'
import { OptimizationRoot } from '@contentful/optimization-react-web'
import { NextPagesAutoPageTracker } from '@contentful/optimization-react-web/router/next-pages'
export default function App({ Component, pageProps }: AppProps) {
return (
<OptimizationRoot
clientId="your-client-id"
environment="main"
api={{
insightsBaseUrl: 'https://ingest.insights.ninetailed.co/',
experienceBaseUrl: 'https://experience.ninetailed.co/',
}}
>
<NextPagesAutoPageTracker />
<Component {...pageProps} />
</OptimizationRoot>
)
}Mount NextPagesAutoPageTracker once inside your provider tree, typically in pages/_app.tsx. The
adapter waits for router.isReady, emits on the first eligible render, emits on route changes, and
suppresses duplicate consecutive router.asPath values.
Automatic page events can be enriched with static and dynamic payloads before calling
optimization.page(...).
<NextPagesAutoPageTracker
pagePayload={{
properties: {
appSection: 'storefront',
},
}}
getPagePayload={({ context, isInitialEmission }) => ({
locale: isInitialEmission ? 'en-US' : undefined,
properties: {
path: context.asPath,
routePattern: context.pathname,
slug: Array.isArray(context.query.slug) ? context.query.slug.join('/') : context.query.slug,
},
})}
/>pagePayloadis included in every auto-emitted page event.getPagePayloadruns once per emitted page event with route-aware context.- Static and dynamic payloads are merged before
optimization.page(...)is called. - When the same field exists in both payloads, the dynamic payload wins.
- This feature is implemented through page payload composition only; no interceptor setup is required or documented for it.
The package dev/ harness keeps the host HTML shell and rsbuild config at the top level, with the
React app itself under dev/app/. It mounts the React Router adapter for interactive local
verification. Other router adapters are still covered primarily through unit tests and the
integration examples above.
The Next.js App Router adapter:
'use client'
import { OptimizationRoot } from '@contentful/optimization-react-web'
import { NextAppAutoPageTracker } from '@contentful/optimization-react-web/router/next-app'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<OptimizationRoot
clientId="your-client-id"
environment="main"
api={{
insightsBaseUrl: 'https://ingest.insights.ninetailed.co/',
experienceBaseUrl: 'https://experience.ninetailed.co/',
}}
>
<NextAppAutoPageTracker />
{children}
</OptimizationRoot>
)
}Mount NextAppAutoPageTracker once in a client component inside your App Router provider tree,
typically via a providers.tsx wrapper used by app/layout.tsx. The adapter emits on the first
eligible render and on pathname + search changes.
<NextAppAutoPageTracker
pagePayload={{
properties: {
appSection: 'storefront',
},
}}
getPagePayload={({ context, isInitialEmission }) => ({
locale: isInitialEmission ? 'en-US' : undefined,
properties: {
path: context.url,
pathname: context.pathname,
search: context.search,
},
})}
/>App Router payload enrichment follows the same payload-composition behavior as the Pages Router adapter and does not use interceptors.
The React Router adapter:
import { createBrowserRouter, Outlet, RouterProvider } from 'react-router-dom'
import { OptimizationRoot } from '@contentful/optimization-react-web'
import { ReactRouterAutoPageTracker } from '@contentful/optimization-react-web/router/react-router'
export function AppLayout() {
return (
<OptimizationRoot
clientId="your-client-id"
environment="main"
api={{
insightsBaseUrl: 'https://ingest.insights.ninetailed.co/',
experienceBaseUrl: 'https://experience.ninetailed.co/',
}}
>
<ReactRouterAutoPageTracker />
<Outlet />
</OptimizationRoot>
)
}
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
children: [
{
index: true,
element: <HomePage />,
},
],
},
])
export function AppRouter() {
return <RouterProvider router={router} />
}Mount ReactRouterAutoPageTracker once inside the react-router-dom router tree and inside the
optimization provider tree, typically in your root layout route. The adapter currently depends on
useMatches(), so it must run under a React Router data router such as createBrowserRouter with
RouterProvider, not a plain BrowserRouter. It emits on the first render and on
pathname + search + hash changes.
<ReactRouterAutoPageTracker
pagePayload={{
properties: {
appSection: 'storefront',
},
}}
getPagePayload={({ context, isInitialEmission }) => ({
locale: isInitialEmission ? 'en-US' : undefined,
properties: {
hash: context.hash,
matchCount: context.matches.length,
path: context.url,
pathname: context.pathname,
},
})}
/>React Router payload enrichment uses the same page-payload composition behavior and does not use interceptors.
The TanStack Router adapter:
import { Outlet } from '@tanstack/react-router'
import { OptimizationRoot } from '@contentful/optimization-react-web'
import { TanStackRouterAutoPageTracker } from '@contentful/optimization-react-web/router/tanstack-router'
export function RootLayout() {
return (
<OptimizationRoot
clientId="your-client-id"
environment="main"
api={{
insightsBaseUrl: 'https://ingest.insights.ninetailed.co/',
experienceBaseUrl: 'https://experience.ninetailed.co/',
}}
>
<TanStackRouterAutoPageTracker />
<Outlet />
</OptimizationRoot>
)
}Mount TanStackRouterAutoPageTracker once inside the TanStack router tree and inside the
optimization provider tree, typically in your root route component. The adapter emits on the first
render and on TanStack Router location.href changes.
<TanStackRouterAutoPageTracker
pagePayload={{
properties: {
appSection: 'storefront',
},
}}
getPagePayload={({ context, isInitialEmission }) => ({
locale: isInitialEmission ? 'en-US' : undefined,
properties: {
hash: context.hash,
matchCount: context.matches.length,
path: context.url,
pathname: context.pathname,
search: context.search,
},
})}
/>TanStack Router payload enrichment also uses page-payload composition only and does not require interceptors.
import { OptimizedEntry } from '@contentful/optimization-react-web'
;<OptimizedEntry baselineEntry={baselineEntry}>
{(resolvedEntry) => <HeroCard entry={resolvedEntry} />}
</OptimizedEntry>OptimizedEntry behavior:
- Default mode locks to the first non-
undefinedoptimization state. liveUpdates={true}enables continuous updates as optimization state changes.- If
liveUpdatesis omitted, global rootliveUpdatesis used. - If both are omitted, live updates default to
false. - Consumer content supports render-prop (
(resolvedEntry) => ReactNode) or directReactNode. - Wrapper element is configurable with
as: 'div' | 'span'(defaults todiv). - Wrapper style uses
display: contentsto remain layout-neutral as much as possible. - Readiness is inferred automatically:
- optimized entries render when
canOptimize === true - baseline entries render when the SDK instance is initialized
- optimized entries render when
When loadingFallback is provided, it is rendered while readiness is unresolved.
<OptimizedEntry
baselineEntry={baselineEntry}
loadingFallback={() => <Skeleton label="Loading optimized content" />}
>
{(resolvedEntry) => <HeroCard entry={resolvedEntry} />}
</OptimizedEntry>- If a baseline entry has optimization references and is unresolved, loading UI is rendered by default.
- If the entry has no optimization references, baseline/resolved content is rendered directly.
- During loading, a concrete layout-target element is rendered (
data-ctfl-loading-layout-target) so loading visibility/layout behavior remains targetable even when wrapper usesdisplay: contents. - During server rendering, unresolved loading is rendered invisibly (
visibility: hidden) to preserve layout space before content is ready.
Nested optimized entries are supported by explicit composition:
<OptimizedEntry baselineEntry={parentEntry}>
{(resolvedParent) => (
<ParentSection entry={resolvedParent}>
<OptimizedEntry baselineEntry={childEntry}>
{(resolvedChild) => <ChildSection entry={resolvedChild} />}
</OptimizedEntry>
</ParentSection>
)}
</OptimizedEntry>Nesting guard behavior:
- Nested wrappers with the same baseline entry ID as an ancestor are invalid and are blocked.
- Nested wrappers with different baseline entry IDs remain supported.
When resolved content is rendered, the wrapper emits attributes used by
@contentful/optimization-web automatic tracking:
data-ctfl-entry-id(always present on resolved content wrapper)data-ctfl-optimization-id(when optimized)data-ctfl-sticky(when available)data-ctfl-variant-index(when optimized)data-ctfl-duplication-scope(when available)
To consume those attributes automatically, enable Web SDK auto-tracking with one of:
autoTrackEntryInteraction: { views: true }duringOptimizationRootinitializationoptimization.tracking.enable('views')/ equivalent runtime setup APIs when applicable
When loadingFallback is shown, resolved-content tracking attributes are not emitted.
Consumers should resolve live updates behavior with:
const isLiveUpdatesEnabled =
liveUpdatesContext.previewPanelVisible ||
(componentLiveUpdates ?? liveUpdatesContext.globalLiveUpdates)This gives:
- preview panel open override first
- component-level
liveUpdatesprop override first - then root-level
liveUpdates - then default
false
- Core/Web SDK initialization is synchronous; no dedicated
sdkInitializedstate is exposed. - React provider initialization outcome is represented by instance creation success/failure.
- The async runtime path is preview panel lifecycle, already represented by preview panel state.
OptimizedEntrynow accepts either render-prop children or directReactNodechildren.- Entries with optimization references now render loading UI until optimization readiness is available.
- When no
loadingFallbackis provided, a default loading UI is rendered for unresolved optimized entries. - Nested wrappers with the same baseline entry ID are now blocked at runtime.
- Loading renders include
data-ctfl-loading-layout-targetfor layout/visibility targeting.
The underlying @contentful/optimization-web SDK enforces a singleton pattern. Only one
ContentfulOptimization runtime can exist at a time (attached to window.contentfulOptimization).
Attempting to initialize a second runtime will throw an error.
When using the config-as-props pattern, the provider uses a useRef to ensure the instance is only
created once, even across React re-renders or StrictMode double-rendering.
When testing components that use the Optimization providers, pass test config props:
import { render } from '@testing-library/react'
import { OptimizationRoot } from '@contentful/optimization-react-web'
render(
<OptimizationRoot
clientId="test-client-id"
environment="main"
api={{
insightsBaseUrl: 'http://localhost:8000/insights/',
experienceBaseUrl: 'http://localhost:8000/experience/',
}}
>
<ComponentUnderTest />
</OptimizationRoot>,
)