Contentful Personalization & Analytics
    Preparing search index...

    Contentful Logo

    Contentful Personalization & Analytics

    React Web SDK Reference Implementation

    Readme · Guides · Reference · Contributing

    Warning

    The Optimization SDK Suite is pre-release (alpha). Breaking changes can be published at any time.

    Reference implementation demonstrating @contentful/optimization-react-web usage in a React SPA. This is the primary React Web reference implementation for customer-style usage of the official React framework package.

    Note

    This implementation is the React Web SDK counterpart to web-sdk_react. Where web-sdk_react builds its own React adapter layer over @contentful/optimization-web, this implementation uses the official @contentful/optimization-react-web framework package directly to match customer integration code. There is no src/optimization/ adapter directory.

    Feature SDK surface used
    Provider + initialization OptimizationRoot
    SPA page tracking ReactRouterAutoPageTracker from @contentful/optimization-react-web/router/react-router
    Entry resolution + rendering OptimizedEntry (render-prop), useEntryResolver().resolveEntry()
    Live updates (global) OptimizationRoot liveUpdates prop
    Live updates (per-component) OptimizedEntry liveUpdates prop
    Live updates (locked) <OptimizedEntry liveUpdates={false}>
    Merge tag rendering useMergeTagResolver().getMergeTagValue()
    Nested personalization Nested <OptimizedEntry> composition
    Consent gating sdk.consent() via useOptimizationContext()
    Identify / reset sdk.identify() / sdk.reset() via useOptimizationContext()
    Auto view/click/hover trackEntryInteraction on OptimizationRoot + data-ctfl-* attributes
    Manual view tracking useOptimization().tracking.enableElement()
    Flag view tracking sdk.states.flag('boolean').subscribe()
    Analytics event stream sdk.states.eventStream.subscribe()
    Preview panel attachment Env-gated attachOptimizationPreviewPanel() call
    Offline queue / recovery Inherited from @contentful/optimization-web runtime

    This app uses default-only SDK contentfulLocales for its single Contentful locale and passes the same locale constant through the provider locale prop. Entry fetches use the recommended withOptimizationLocale() helper so they use the live resolved optimization.locale. Experience API calls use that resolved locale by default unless api.locale is configured as an explicit API override. Do not use contentful.js withAllLocales or raw CDA locale=* for entries passed to OptimizedEntry or useEntryResolver(); 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 for the broader locale model and Entry personalization and variant resolution for the entry contract.

    • Node.js >= 20.19.0 (24.13.0 recommended to match .nvmrc)
    • pnpm 10.x

    From the repository root:

    pnpm build:pkgs
    pnpm implementation:run -- react-web-sdk implementation:install
    test -f implementations/react-web-sdk/.env || cp implementations/react-web-sdk/.env.example implementations/react-web-sdk/.env

    From the repository root:

    1. Start the mock API server:
    pnpm serve:mocks
    
    1. In another terminal, start the development server:
    pnpm implementation:run -- react-web-sdk dev
    
    1. Build for production:
    pnpm implementation:run -- react-web-sdk build
    
    1. Run type checking:
    pnpm implementation:run -- react-web-sdk typecheck
    

    The equivalent implementation-directory commands are:

    pnpm dev
    pnpm build
    pnpm typecheck
    1. Run the full E2E setup and test suite from the repository root:
    pnpm setup:e2e:react-web-sdk
    pnpm test:e2e:react-web-sdk
    1. Or run the Playwright flow step by step:
    pnpm implementation:run -- react-web-sdk serve
    

    In another terminal:

    pnpm --dir implementations/react-web-sdk --ignore-workspace exec playwright test
    

    When finished:

    pnpm implementation:run -- react-web-sdk serve:stop
    

    Copy .env.example to .env:

    cp .env.example .env
    

    All variables have mock-safe defaults. To use local mock endpoints (the default), no changes are needed. PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL="true" attaches the browser preview panel for local and staging development runs. See .env.example for the full list.

    react-web-sdk/
    ├── src/
    │ ├── main.tsx # OptimizationRoot, preview panel attachment, createBrowserRouter
    │ ├── App.tsx # Shared layout: SDK state, entry loading, nav, AnalyticsEventDisplay
    │ ├── components/
    │ │ ├── AnalyticsEventDisplay.tsx # Live event stream panel (persists across routes)
    │ │ └── RichTextRenderer.tsx # Rich text + merge tag rendering
    │ ├── config/
    │ │ ├── entries.ts # Entry ID constants
    │ │ └── routes.ts # Route path constants
    │ ├── pages/
    │ │ ├── HomePage.tsx # Utility panel, live updates, entry sections
    │ │ └── PageTwoPage.tsx # Navigation + conversion tracking demo
    │ ├── sections/
    │ │ ├── ContentEntry.tsx # Auto/manual tracked entry renderer
    │ │ ├── LiveUpdatesExampleEntry.tsx # Live updates parity: default / locked / always-live
    │ │ ├── NestedContentEntry.tsx # Nested personalization wrapper
    │ │ └── NestedContentItem.tsx # Recursive nested entry via OptimizedEntry
    │ ├── services/
    │ │ └── contentfulClient.ts # Contentful CDA client
    │ ├── types/
    │ │ ├── contentful.ts # Entry type definitions
    │ │ └── env.d.ts # import.meta.env typings
    │ └── utils/
    │ └── typeGuards.ts # isRecord helper
    ├── e2e/ # Playwright E2E tests (parity with web-sdk_react)
    ├── index.html
    ├── .env.example
    ├── package.json
    ├── rsbuild.config.ts
    ├── tsconfig.json
    ├── playwright.config.mjs
    ├── AGENTS.md
    └── README.md
    import { OptimizationRoot } from '@contentful/optimization-react-web'
    import { ReactRouterAutoPageTracker } from '@contentful/optimization-react-web/router/react-router'
    import { useState } from 'react'
    import { createBrowserRouter, Outlet, RouterProvider } from 'react-router-dom'

    function RootLayout() {
    const [liveUpdates, setLiveUpdates] = useState(false)

    return (
    <OptimizationRoot
    clientId="your-client-id"
    environment="main"
    api={{
    insightsBaseUrl: 'https://ingest.insights.ninetailed.co/',
    experienceBaseUrl: 'https://experience.ninetailed.co/',
    }}
    trackEntryInteraction={{ views: true, clicks: true, hovers: true }}
    liveUpdates={liveUpdates}
    >
    <ReactRouterAutoPageTracker />
    <Outlet context={{ onToggleLiveUpdates: () => setLiveUpdates((v) => !v) }} />
    </OptimizationRoot>
    )
    }

    const router = createBrowserRouter([
    {
    path: '/',
    element: <RootLayout />,
    children: [
    {
    element: <AppLayout />,
    children: [
    { index: true, element: <HomePage /> },
    { path: 'page-two', element: <PageTwoPage /> },
    ],
    },
    ],
    },
    ])
    import { OptimizedEntry } from '@contentful/optimization-react-web'

    function HeroSection({ entry }) {
    return (
    <OptimizedEntry baselineEntry={entry}>
    {(resolvedEntry) => <p>{resolvedEntry.fields.text}</p>}
    </OptimizedEntry>
    )
    }
    // Global: controlled via OptimizationRoot prop (lifted state in parent)
    <OptimizationRoot liveUpdates={globalLiveUpdates} ...>

    // Per-component: always live regardless of global setting
    <OptimizedEntry baselineEntry={entry} liveUpdates={true}>
    {(resolved) => <Card entry={resolved} />}
    </OptimizedEntry>

    // Per-component: locked to first resolved value
    <OptimizedEntry baselineEntry={entry} liveUpdates={false}>
    {(resolved) => <Card entry={resolved} />}
    </OptimizedEntry>
    import { useOptimizationContext } from '@contentful/optimization-react-web'

    function Controls() {
    const { sdk } = useOptimizationContext()

    return (
    <>
    <button onClick={() => sdk.consent(true)}>Accept</button>
    <button onClick={() => void sdk.identify({ userId: 'u1', traits: { identified: true } })}>
    Identify
    </button>
    <button onClick={() => sdk.reset()}>Reset</button>
    </>
    )
    }
    import { useEntryResolver, useOptimization } from '@contentful/optimization-react-web'
    import { useEffect, useRef } from 'react'

    function ManuallyTrackedEntry({ entry }) {
    const sdk = useOptimization()
    const { resolveEntry } = useEntryResolver()
    const ref = useRef(null)
    const resolved = resolveEntry(entry)

    useEffect(() => {
    const el = ref.current
    if (!el) return
    sdk.tracking.enableElement('views', el, { data: { entryId: resolved.sys.id } })
    return () => sdk.tracking.clearElement('views', el)
    }, [resolved.sys.id, sdk.tracking])

    return <div ref={ref}>{String(resolved.fields.text)}</div>
    }

    For entries tracked via trackEntryInteraction, apply data-ctfl-* attributes directly on the visible content element inside the render prop:

    <OptimizedEntry baselineEntry={entry}>
    {(resolvedEntry) => (
    <div
    data-ctfl-entry-id={resolvedEntry.sys.id}
    data-ctfl-baseline-id={entry.sys.id}
    data-ctfl-hover-duration-update-interval-ms="1000"
    data-ctfl-clickable="true" // mark as click target
    >
    {String(resolvedEntry.fields.text)}
    </div>
    )}
    </OptimizedEntry>
    Note

    The OptimizationRoot trackEntryInteraction prop activates automatic view, click, and hover tracking for any DOM element that has data-ctfl-entry-id. The SDK's MutationObserver registers elements as they appear in the DOM after consent is given.

    File or area Purpose
    src/main.tsx Configures OptimizationRoot and ReactRouterAutoPageTracker
    src/App.tsx Subscribes to provider state and renders route-level controls
    src/sections/ContentEntry.tsx Demonstrates OptimizedEntry, useEntryResolver(), and tracking
    src/sections/LiveUpdatesExampleEntry.tsx Demonstrates locked and live entry resolution
    src/components/RichTextRenderer.tsx Demonstrates merge tag rendering with useMergeTagResolver()
    src/components/AnalyticsEventDisplay.tsx Displays event stream output from sdk.states.eventStream
    Manual selectedOptimizations lock logic <OptimizedEntry liveUpdates={false}>

    What stays the same: contentfulClient.ts, entry/route config, type definitions, RichTextRenderer, E2E test files, page/section component structure.

    Key architectural difference: App.tsx acts as a persistent layout (contains AnalyticsEventDisplay that stays mounted across route changes). Pages are route children that receive state via useOutletContext.