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 contextOptimizationRoot provider composition and defaultsuseOptimizedEntry() imperative optimization resolutionOptimizedEntry entry 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.
Run the one-shot launcher to start the mock server and dev harness in a single command:
./scripts/launch-dev-harness.sh
Or via pnpm:
pnpm --filter @contentful/optimization-react-web dev:launch
This installs dependencies (if needed), creates .env from dev/.env.example, starts the mock API
server on port 8000, and launches the Rsbuild dev server with hot reload across the full SDK stack
(React SDK, Web SDK, Core SDK, API Client, API Schemas). No build step is needed.
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 dev
From this package directory:
pnpm build
pnpm typecheck
pnpm test:unit
pnpm dev
rslib/rsbuild/rstest/TypeScript baseline aligned with Web SDK patternssrc/OptimizedEntry component with loading-state support and Web SDK data-attribute trackingdev/ and the React app in dev/app/ for
consent, identify/reset, state, events, and entriesPass 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)LiveUpdatesProvideruseOptimization() returns the initialized ContentfulOptimization instance.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 outside OptimizationProvider.useOptimization() also throws if the provider exists but the SDK is not ready.useLiveUpdates() throws if used outside LiveUpdatesProvider.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,
},
})}
/>
pagePayload is included in every auto-emitted page event.getPagePayload runs once per emitted page event with route-aware context.optimization.page(...) is called.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:
undefined optimization state.liveUpdates={true} enables continuous updates as optimization state changes.liveUpdates is omitted, global root liveUpdates is used.false.(resolvedEntry) => ReactNode) or direct ReactNode.as: 'div' | 'span' (defaults to div).display: contents to remain layout-neutral as much as possible.canOptimize === trueWhen loadingFallback is provided, it is rendered while readiness is unresolved.
<OptimizedEntry
baselineEntry={baselineEntry}
loadingFallback={() => <Skeleton label="Loading optimized content" />}
>
{(resolvedEntry) => <HeroCard entry={resolvedEntry} />}
</OptimizedEntry>
data-ctfl-loading-layout-target)
so loading visibility/layout behavior remains targetable even when wrapper uses
display: contents.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:
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 } during OptimizationRoot initializationoptimization.tracking.enable('views') / equivalent runtime setup APIs when applicableWhen 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:
liveUpdates prop override firstliveUpdatesfalsesdkInitialized state is exposed.OptimizedEntry now accepts either render-prop children or direct ReactNode children.loadingFallback is provided, a default loading UI is rendered for unresolved optimized
entries.data-ctfl-loading-layout-target for 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>,
)