> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://contentful.com/developers/docs/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://contentful.com/_mcp/server. # Experience SDK > Provide Experiences within your Javascript-based front-end. > **Info** > > This Experience SDK is for Contentful Personalization use. If you are looking for the Experiences SDK for studio, see the [Studio documentation](/experiences/overview). ## Overview We provide SDKs and plugins for modern web frameworks to enable you to use Contentful Personalization. Our SDKs include first-class TypeScript support with exported type definitions for a smooth development experience in TypeScript projects. The SDKs provide abstracted means to communicate with the Experience API and the browser. It shortens integration times into JavaScript codebases. The SDKs handle: * Creating `page`, `track`, and `identify` events and sending them to the Experience API * Providing built-in capabilities to handle errors, retries and queuing. * Enabling caching of the profile client-side via `localStorage`. ## Composed JavaScript SDKs Our SDKs have three levels of abstraction. At the lowest-level, the Shared SDK `@ninetailed/experience.js-shared` creates an API Client that communicates with the Experience API's endpoints. The data objects, types, and methods it provides are applicable across all SDKs. The [JavaScript SDK](https://www.npmjs.com/package/@ninetailed/experience.js) `@ninetailed/experience.js` is based on the Shared SDK. It creates an instance that stores and updates a profile in response to events. ### Front-end SDKs The Shared and JavaScript SDKs can be used to integrate Contentful Personalization with arbitrary front-ends that run JavaScript. For fast integration with popular front-end frameworks, we provide SDKs for: * [React (@ninetailed/experience.js-react)](https://www.npmjs.com/package/@ninetailed/experience.js-react) * [Next.js (@ninetailed/experience.js-next)](https://www.npmjs.com/package/@ninetailed/experience.js-next) Additionally, Contentful Personalization provides a [Node.js SDK](https://www.npmjs.com/package/@ninetailed/experience.js-node) for interacting with the Experience API in server or edge runtimes. ### Server-side SDK The [Node.js SDK (@ninetailed/experience.js-node)](https://www.npmjs.com/package/@ninetailed/experience.js-node) is provided for integrating Contentful Personalization in server or edge runtimes. ## Choose an SDK > **Info** > > We strongly recommend using the SDK available for your framework, if one is available, for the fastest, most declarative implementation. For most Contentful Personalization users, it's enough to install the appropriate React-based SDK, send `page`, `track`, and `identify` events to enhance user profiles, and use the `` component to render personalization and experiment content client-side. However, you may need to arbitrarily access profile data or exercise more control over how experiences are rendered and tracked. These needs usually arise when: 1. You need more control within a React-based web application, like if you're edge- or server-side rendering experiences and/or using React Server Components; or 2. You're not implementing Contentful Personalization within a web-based React project (e.g., Vue/Nuxt, Svelte or React Native). In such scenarios, the API Client created by the Shared SDK serves as the best starting place for upserting profiles in server and edge runtimes, while the JavaScript SDK provides declarative tracking behaviour to use in client-side code. For any front-end integration that is not based on JavaScript, explore our [Experience API](/personalization/experience-api) for complete freedom on where to integrate. ## Install the SDK ### Dependency installation #### React ```sh npm install @ninetailed/experience.js-react # OR yarn add @ninetailed/experience.js-react ``` #### Next.js ```sh npm install @ninetailed/experience.js-next # OR yarn add @ninetailed/experience.js-next ``` #### JavaScript ```bash npm install @ninetailed/experience.js # OR yarn add @ninetailed/experience.js ``` ### Create an instance At its core, the Experience SDK defines a `Ninetailed` class instance based on your input configuration. That instance then: * Keeps track of the current profile and provides a hook into profile changes. * Exposes declarative methods to interact with the Experience API. * Provides extensibility through plugins. When working with the React or Next.js SDK, an instance will be created and made available to your application globally through a React context provider. > **Info** > > Your API key is the **Client ID** field value displayed under the "SDK Keys" section on the **Optimization** tab. > > ![Client ID](https://fdr-prod-docs-files-public.s3.us-east-1.amazonaws.com/contentful.docs.buildwithfern.com/d9c1e92409fed1e1c97c28fd1fc9f7c5e1fcfcdc19c22cc05eba86b166a94a5f/docs/assets/images/client-id.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIA6KXJSKKNFOCF7G4B%2F20260929%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260929T234802Z&X-Amz-Expires=604800&X-Amz-Signature=a2db1bd64a4b4151287cc5cd832bdc64430fe50a6aa2847e7b8d29dc88d2d700&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject) #### React Add the `` component to the top level of the application, so that the profile can be consumed from any child component. ```jsx import React from 'react'; import MyAppCode from '../myAppCode.jsx'; import { NinetailedProvider } from '@ninetailed/experience.js-react'; export const App = () => { return (
component must be present in the viewport to register a component view componentViewTrackingThreshold={2000} // Specify a maximum amount of time (ms) to wait for an Experience API response before falling back to baseline content requestTimeout={5000} // Specify a locale to localize profile location information locale="en-US" // Specify an alternative Experience API base URL url="https://experience.ninetailed.co" // Set to true ONLY if using an unindexed CMS useSDKEvaluation=true >
); } ``` #### Next.js > **Info** > > We recommend adding the `` component to the top level of the application. This way, the profile can be consumed from any child component. The Next.js `` also hooks into the Next page router and automatically calls `ninetailed.page()` on every route change. Do **not** additionally call this method on your own, otherwise you will duplicate events. ```jsx import { NinetailedProvider } from '@ninetailed/experience.js-next'; export const App = ({component, pageProps}) => { return (
component must be present in the viewport to register a component view componentViewTrackingThreshold={2000} // Specify a maximum amount of time (ms) to wait for an Experience API response before falling back to baseline content requestTimeout={5000} // Specify a locale to localize profile location information locale="en-US" // Specify an alternative Experience API base URL url="https://experience.ninetailed.co" // Set to to true ONLY if using an unindexed CMS useSDKEvaluation=true >
); } ``` #### JavaScript ```javascript import { Ninetailed } from '@ninetailed/experience.js'; export const ninetailed = new Ninetailed( { // REQUIRED. An API key uniquely identifying your Ninetailed account. clientId: "NINETAILED_API_KEY", // OPTIONAL. Your Ninetailed environment, typically either "main" or "development" environment: "main" // Default }, // === THE FOLLOWING ARGUMENT AND ALL OF ITS PROPERTIRES ARE OPTIONAL === // === DEFAULT VALUES ARE SHOWN === { // Add any plugin instances plugins: [], // Specify an amount of time (ms) that a component must be present in the viewport to register a component view componentViewTrackingThreshold: 2000, // Specify a maximum amount of time (ms) to wait for an Experience API response before falling back to baseline content requestTimeout: 5000, // Specify a locale to localize profile location information locale: "en-US", // Specify an alternative Experience API base URL url: "https://experience.ninetailed.co" // Set to to true ONLY if using an unindexed useSDKEvaluation: true } ); ``` ### Browser instance access Installing the Experience SDK exposes several Contentful Personalization properties and methods on a `window.ninetailed` object to facilitate testing and debugging. | Properties and methods | Description | | ------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `page()`, `track()`, and `identify()` | The core tracking methods. | | `debug(arg: boolean)` | Turn on debug mode, which will output additional information about your experiences assignment to the console. | | `plugins` | Access the methods of any plugins attached to the instance. | | `profile` | Output the current profile state. | | `reset()` | Discard the current profile. | For a full description of instance properties and methods available to your application, consult the [JavaScript SDK](/personalization/the-ninetailed-instance) documentation. ## Send events Contentful Personalization profiles are created and updated by sending events about that profile to the Experience API. Rather than interacting with the Experience API endpoints directly, the principal way to send events is to use the methods made available from the [JavaScript SDK](/personalization/the-ninetailed-instance). > **Info** > > Interacting with the Experience API endpoints directly is possible and necessary for integration with applications that do not support JavaScript. A class instance created by any of Contentful Personalization's SDKs composed from the JavaScript SDK provides three methods for sending events: `page`, `track` and `identify`. > **Info** > > You can validate your implementation by observing all incoming events with [Live Events](https://www.contentful.com/help/personalization/live-events/) in the Optimization tab of the Contentful web app. ### `page` events ```typescript type Page = (properties?: Object) => Promise; ``` A `page` event indicates a user has viewed the current page. The SDK provides a context object describing the parameters of the page that has been viewed, including the `referrer`, `url`, `path`, `user-agent` and other properties to be consumed by the API. While most of the time you will call this `page` method with no arguments, you may optionally specify any properties you want to send alongside with the page view event. This can be useful when creating Audiences. For example, if the category to which viewed blog posts belong is not contained within the URL of the blog posts but you'd like to create an Audience of visitors who have viewed blog posts of a certain category a certain number of times, you can pass the category along in the argument: ```javascript page({'category': 'YOUR_BLOG_CATEGORY'}) ``` > **Info** > > **NOTES**: > > * The Next.js SDK also sends `page` events automatically on page changes, but only if you're using Page Router. If you're using App Router, you have to set up the logic for sending the event. [Here is an example](https://github.com/ninetailed-inc/ninetailed-examples/blob/main/marketing-contentful-next-app/app/layout.tsx#L43) where the event is sent in the `TrackPages` component for App Router. > * If your application uses the React, JavaScript or Node SDK, you have to manually implement a `page` call on every route change. ### track events ```typescript type Track = (event: string, properties?: Object) => Promise; ``` A `track` event logs specific user actions, such as `signup` or `registered_for_newsletter`. It can be arbitrarily named and accepts a `properties` argument that enriches the event with additional information. For example, you might include the SKU and quantity of items on a `track` event called `"add_to_cart"`. ```javascript track('add_to_cart', {sku: '9T41L', quantity: 1}) ``` In addition to serving as an Audience rule, `track` events are used to indicate the conversion events you want to measure in Experience Insights. ### identify events ```typescript type Identify = (uid: string, traits?: Traits) => Promise; ``` `Identify` events serve two purposes: First, `identify` allows you to add custom information, called traits, to a profile. Traits can be any attribute of interest about a customer. Any valid JSON is a valid trait. You can store any information of interest on profiles that you may want to use to segment users. For example, you might store a user's responses from a sign up form, a list of products they recently visited, or data from an upstream source of your customer data like a CRM or CDP. ```javascript identify('', { favoriteColor: "red" }) ``` Second, `identify` allows you to name or "alias" a profile such that it can be referenced by that same alias in the future. IDs stored within an analytics system, customer data platform, or e-commerce platform make ideal aliases. ```javascript identify('customer12345') ``` After aliasing a profile, you can reinstate the aliased profile on a different device or browser by calling `identify` again using the same supplied alias. This merges the latest activity of the current profile with the profile at that alias. For personalizations and experiments served to logged in users, you will likely want to call `identify` after each successful authentication. > **Info** > > The flexibility of `identify` is powerful but should be used with caution. In particular, you should consider what privacy legislation your application needs to abide by, and whether that affects what data should not be stored as traits. For example, you probably would not want to store contact information, such as email addresses or phone numbers as traits, or use them as aliases. ### component events A `component` event records every time a user views a Contentful content entry on a website, as well as how long they viewed it for. It includes contextual details such as the entry ID, timestamp, device type and environment. `component` events are automatically sent by the SDK. Component events: * Track the total number of views for each component. * Track the amount of time that each component was viewed for. * Attribute conversions to specific components. * Analyze the impact of content at the component level. * Support Contentful Personalization by linking views to specific experiences or variants. ### component click events A `component_click` event records every time a user clicks an interactive element inside of a Contentful content entry on a website. It includes contextual details such as the entry ID, timestamp, device type and environment. `component_click` events are enabled by adding the `trackClicks` prop to an `` component. Component click events: * Track the total number of times each component was clicked. * Analyze the impact of content at the component level. ### component hover events A `component_hover` event records every time a desktop user hovers an element inside of a Contentful content entry on a website, as well as how long they hovered it for. It includes contextual details such as the entry ID, timestamp, device type and environment. `component_hover` events are enabled by adding the `trackHovers` prop to an `` component. Component hover events: * Track the total number of times each component was hovered. * Analyze the impact of content at the component level. ### Access event methods #### React, Next.js The `useNinetailed` hook provides the tracking functions `page`, `track`, and `identify`. These methods will call the Experience API using the configuration parameters supplied to a ``. ```jsx import React from 'react'; import { useNinetailed } from '@ninetailed/experience.js-react'; // or import { useNinetailed } from '@ninetailed/experience.js-next'; export const myComponent = () => { const { page, track, identify } = useNinetailed(); page(); identify('anAlias', {someTrait: "value"}); return ( ); } ``` #### Javascript The `page`, `track`, and `identify` methods are available directly on a Ninetailed class instance. ```javascript import { Ninetailed } from '@ninetailed/experience.js'; const ninetailed = new Ninetailed({ clientId: "NINETAILED_API_KEY"}) ninetailed.page(); ninetailed.track('myEvent') ninetailed.identify('alias', {traitName: "traitValue"}) ``` ## Render analytics entries When using one of the React SDKs, Contentful entries must be rendered as children of an `` component, or an `` component for analytics to work correctly. ### The `` component The React-based SDKs provide an `` component for rendering analytics entries. > **Info** > > This component is a part of the Experience React SDK, introduced in version `7.17.6`. The `` component functions wraps your existing React component. It automatically detects the properties needed from the wrapped component. | Prop | Required | Description | | :----------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | | `{...props}` | Yes | All props the `component` prop requires to render the entry. | | `id` | Yes | The Contentful entry ID of the entry. | | `component` | Yes | The React component that your entry will use to render. This can either be a regular React component, or a component that uses React's `forwardRef`. | #### Example use ```tsx // or '@ninetailed/experience.js-next' import { EntryAnalytics } from '@ninetailed/experience.js-react'; // This function is assumed to return a single entry and all its supporting data. import { getCmsEntry } from '../api/yourEntryGetter' import { YourComponent } from './YourComponent' export const YourAnalytics = (contentfulEntry) => { const entry = getCmsEntry(contentfulEntry); return ( ); }; ``` ### Custom flag change types Custom flag change types provide an alternative way to manage changes within a personalization or experiment, using an ID in your code paired with plain text (string) or JSON (structured format) to make the change. Using the `` hook, you can fetch the changes from a `profileState`. We are accepting the changes that may come with the profile that is returned from the Experience API and handle that in the hook. **Parameters** | Name | Description | | ----------------- | ---------------------------------------------- | | `flagKey` | The unique identifier for your custom flag. | | `defaultValue` | The fallback value if the flag is unavailable. | | `shouldAutoTrack` | Used to enable performance tracking of views. | Returns: * `value`: The current flag value. * `status`: 'loading' | 'success' | 'error'. * `error`: Error object or null. > **Info** > > If you use the same `flagKey` across multiple Experiences on your website and the end-user visiting your site is a part of more than one Personalization that share the same `flagKey`, they are exposed to the one with the alphabetically lower `entryId` value. This means you cannot define an order of priority for your Experiences. **Example** #### Basic usage ```jsx import { useFlag } from '@ninetailed/experience.js-react'; function MyComponent() { const { value, status } = useFlag('my-feature-flag', false); if (status === 'loading') { return
Loading...
; } return (
{value ? (
New Feature Enabled!
) : (
Standard Experience
)}
); } ``` #### JSON flags ```jsx import { useFlag } from '@ninetailed/experience.js-react'; function PersonalizedHero() { const { value } = useFlag('hero-content', { headline: 'Welcome to our platform', subheadline: 'Discover amazing features', cta: 'Get Started', image: '/default-hero.jpg' }); return (

{value.headline}

{value.subheadline}

Hero
); } ``` #### Track views for custom flags You can track the performance of your custom flag views by passing the `shouldAutoTrack` option to the `useFlag` hook, so you can analyze the impact of the dynamic content managed through the variable flags. It accepts a boolean or a function returning a boolean. > **Info** > > Tracking is optional and only triggered if the custom flag is resolved from the [Experience API](/personalization/experience-api) and you explicitly enable tracking using the `shouldAutoTrack` option. No tracking is sent for fallback values. **Example 1: Always track when resolved** ```javascript const { value, status } = useFlag('theme-variable', { padding: '12px', color: 'black', }, { shouldAutoTrack: true }); ``` **Example 2: Conditionally track based on state** ```javascript const { value, status } = useFlag('theme-variable', { padding: '12px', color: 'black', }, { shouldAutoTrack: () => isUserLoggedIn() && hasCompletedOnboarding() }); ``` #### Manual tracking If your personalization only becomes visible after a user interaction, for example, a modal shown on button click, you likely want to track the "view" of a custom flag at a later, custom point in time. For this purpose, you can use the following option: `useFlagWithManualTracking`. This ensures tracking is sent only when the user is actually affected by the flag. Example: ```javascript const [flag, track] = useFlagWithManualTracking('modal-variant', 'default'); const handleSaveClick = () => { openModal(flag); track(); // Only now the user has seen the personalization }; ``` #### Tracked metadata When a view is tracked for a Variable, the SDK sends a `componentView` event with: | **Field** | **Value** | | :-------------- | :------------------------------------ | | `componentType` | `Variable` | | `componentId` | `Variable-{flagKey}` | | `experienceId` | Experience ID from the resolved flag. | | `variantIndex` | Index of the resolved variant. | ### Validate values at runtime When providing JSON values for a custom flag, you must make sure the defined values are valid. For that, we can use runtime validation packages like [zod](https://zod.dev/). #### Example: Button colors In this example, we want to test **button colors**. To do so, we create a new Experiment in Contentful Personalization and use Custom flags with a `JSON` type. We then define the values as needed: ![Custom flags with a JSON type](https://fdr-prod-docs-files-public.s3.us-east-1.amazonaws.com/contentful.docs.buildwithfern.com/aa9a77570aafbf5ee897e554cfef09fafd1e613235010955b1e453f76e1ee253/docs/assets/images/custom-flags-json-type.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIA6KXJSKKNFOCF7G4B%2F20260929%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260929T234802Z&X-Amz-Expires=604800&X-Amz-Signature=6ee626eb1109ac1f31af7de3166e40940c297d22a4863f31b39ae7c0768266f4&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject) In code, we need to use these values and validate them at runtime to ensure that our visitors don't encounter any errors in production: ```json import * as z from "zod"; // Zod schema for runtime validation (making sure it's a HEX value like #ABC123) const ButtonStyleSchema = z.object({ backgroundColor: z.string().regex(/^#[0-9a-f]{6}$/i), color: z.string().regex(/^#[0-9a-f]{6}$/i), }); const ButtonMapSchema = z.object({ primary: ButtonStyleSchema, secondary: ButtonStyleSchema, }); type ButtonMap = z.infer; // define the default values (you could also do that within useFlag) const defaultButtonMap: ButtonMap = { primary: { backgroundColor: '#4f46e5', color: '#ffffff' }, secondary: { backgroundColor: '#e0e7ff', color: '#4338ca' }, }; // capturing the real value const { value: rawButtonMapValue } = useFlag( 'buttonColors', defaultButtonMap, ); // Validate the variant map and fall back to default if invalid const validationResult = ButtonMapSchema.safeParse(rawButtonMapValue); const buttonMap: ButtonMap = validationResult.success ? validationResult.data : defaultButtonMap; ``` > **Info** > > For simplicity reasons, the example above doesn't include some of the capabilities that you would usually also include, such as: > > * Tracking using the `{shouldAutoTrack: true}` option or the `useFlagWithManualTracking` hook, to correctly track the performance of an optimization. > * Handling loading states with the provided status value. ## Utility libraries Our Experience API utility libraries provide methods to map experience content to the format required by the `` component exported by our React and Next.js SDK. Use the [Contentful Utility SDK](#contentful-library-usage) if you are retrieving content and experiences using Contentful's client libraries that interface with the Contentful REST APIs, including: * the Contentful Content Delivery API * the Contentful Content Preview API For all other sources, including the Contentful GraphQL API or your own internal content APIs, middleware, use the [JavaScript Utility SDK](#javascript-library-usage) and map your experiences to the type required by the `ExperienceMapper` class methods. ### JavaScript library usage ```sh npm install @ninetailed/experience.js-utils # OR yarn add @ninetailed/experience.js-utils ``` You must map your fetched CMS Experience entries to a particular shape prior to transforming them with the `ExperienceMapper` methods. The following examples show the format required in a `.map` step prior to calling `.filter` to remove ill-formatted entries. #### Generic ```javascript import { ExperienceMapper } from '@ninetailed/experience.js-utils'; const mappedExperiences = (myEntry.nt_experiences || []) .map((experience) => { return { id: experience.id, name: experience.name type: experience.nt_type as 'nt_personalization' | 'nt_experiment' config: experience.nt_config, audience: { id: experience.nt_audience.nt_audience_id // If mapping for the Preview Plugin, this displays audience names name: experience.nt_audience.nt_name }, variants: experience.variants.map((variant) => { return { id: variant.id, // Required // Map any other fields required by your components ...variant, someComponentProp: variant.foo } }) } }) .filter((experience) => ExperienceMapper.isExperienceEntry(experience)) .map((experience) => ExperienceMapper.mapExperience(experience)); ``` #### Contentful GraphQL API Your exact query and mapping will vary depending on both your content model and the props required by the component you use to render your content. This example assumes a content model using a content type of `page` that contains a field called `sections` that can reference entries of type `hero`. It also shows using a lightweight GraphQL client library `graphql-request` to make the API request, but any GraphQL client is suitable. ```javascript import { request, gql } from "graphql-request"; import { filterAndMapExperiences, mapAudiences } from "../lib/helpers"; const CONTENTFUL_HERO_QUERY = gql` fragment SysId on Entry { sys { id } } fragment HeroEntry on Hero { ...SysId internalName } fragment NtExperienceFields on NtExperience { ...SysId ntExperienceId ntName ntType ntConfig ntAudience { ntAudienceId } } fragment NinetailedHero on Hero { ...HeroEntry ntExperiencesCollection(limit: 10) { items { ...NtExperienceFields ntVariantsCollection(limit: 10) { items { ...HeroEntry } } } } } query NinetailedHeroQuery($heroEntryid: String!) { page(id: $heroEntryid) { ...SysId sectionsCollection(limit: 10) { items { ...NinetailedHero } } } } `; export async function getHeroData(heroId) { const data = await request( `https://graphql.contentful.com/content/v1/spaces/${process.env.CTFL_SPACE_ID}`, CONTENTFUL_HERO_QUERY, heroId, { Authorization: `Bearer ${process.env.CTFL_API_KEY}`, "Content-Type": "application/json", } ); return data; } ``` ```javascript import { ExperienceMapper } from '@ninetailed/experience.js-utils'; import { getHeroData } from 'api/yourDataFetcher'; const hero = await getHeroData('aHeroEntryId') const mappedExperiences = (hero.ntExperiencesCollection?.items || []) .map((experience) => { return { id: experience.ntExperienceId, name: experience.ntName, type: experience.ntType, config: experience.ntConfig, // This syntax accounts for the possibility of an audience not being set on an experiment ...(experience.ntAudience ? { audience: { id: experience.ntAudience.ntAudienceId // If mapping for the Preview Plugin, this displays audience names name: experience.ntAudience.ntName }, } : {}) variants: experience.ntVariantsCollection.items.map((variant) => { return { id: variant.sys.id, // Required // Map any other fields required by your rendering component ...variant } }) } }) .filter((experience) => ExperienceMapper.isExperienceEntry(experience)) .map((experience) => ExperienceMapper.mapExperience(experience)); ``` Notice the use of fragments to capture the `sys.id`, since this is required on the Ninetailed Experience (`NtExperience`) entry as well as all variants referenced by the entry. Additionally, note the use of a fragment to isolate the fields of the Experience entry so that the base `HeroEntry` fragment can be used to query both the baseline and the variant content without introducing a circular reference. ### Contentful library usage ```bash npm install @ninetailed/experience.js-utils-contentful # OR yarn add @ninetailed/experience.js-utils-contentful ``` ```javascript import { ExperienceMapper } from '@ninetailed/experience.js-utils-contentful' import { createClient } from 'contentful'; const client = createClient({ accessToken: 'youtAccessToken', space: 'yourSpaceId' }) // Specify what entries with Ninetailed Experience references to get from Contentful const query = {...} const rawEntries = await client.getEntries(query); // Extract one entry, as an example const [yourEntry] = rawEntries.items // Filter and map with ExperienceMapper methods const experiences = (yourEntry.fields.nt_experiences || []) .filter(ExperienceMapper.isExperienceEntry) .map(ExperienceMapper.mapExperience) ``` See also our [Contentful + Next.js example project](https://github.com/ninetailed-inc/ninetailed-examples/blob/main/marketing-contentful-next/lib/experiences.ts) for context. ### ExperienceMapper class methods #### isExperienceEntry Determines if a provided entry is of valid type to be consumed by `mapExperience`. Use with `.filter` to remove any invalidly typed experiences. #### mapExperience Transform an experience to the type required by the `` component. #### isExperimentEntry Determines if a provided entry is of valid type to be consumed by `mapExperiment`. Use with `.filter` to remove any invalidly typed experiments. #### mapExperiment Transform an experiment to the type required by the React and Next.js `` `experiments` prop. #### mapCustomExperience This class method is for the Contentful library only. If you need to modify how the variants referenced by an experience entry retrieved from Contentful are mapped, use this method to pass a custom variant mapping function. Example usage: ```tsx const experiences = myExperience.fields.nt_experiences .filter(ExperienceMapper.isExperienceEntry) .map((experience) => { ExperienceMapper.mapCustomExperience(experience, (variant) => { id: variant.sys.id // required // Add any data required by your `component` prop on the component ...variant.fields, someComponentProp: variant.foo }); }) ``` #### mapCustomExperienceAsync This class method is for the Contentful library only, SDK >= 7.7.x. Similar to `mapCustomExperience`, but allows asynchronous operations to be executed in the variant mapping step. ```tsx const experiences = myExperience.fields.nt_experiences .filter(ExperienceMapper.isExperienceEntry) .map((experience) => { ExperienceMapper.mapCustomExperienceAsync(experience, async (variant) => { await new Promise ((resolve) => setTimeout(resolve, 1000)); // Simulated delay supported by async handler return { id: variant.sys.id // required // Add any data required by your `component` prop on the component ...variant.fields, someComponentProp: variant.foo } }); }) ``` #### mapBaselineWithExperiences This class method is for the Contentful library only. Supply an object representing a baseline entry and its attached experiences and return an array of filtered and mapped experiences. ## Render Experiences ### The `` component The React-based SDKs provide an `` component to wrap your existing React components you use to render your content. This is the most declarative way to render personalization and experiment content, and therefore the methodology that most users should adopt when possible. If you track your content with the [Insights plugin](/personalization/insights-plugin), it is recommended to wrap **all** of your content entries with the `` component. This allows you to track content on a component level, even if they are not connected to a personalization or experiment. The `` component automatically detects the properties needed from the wrapped component. The `experiences` prop accepts Experience content that has been appropriately transformed by the `ExperienceMapper` available from our [Utility libraries](#utility-libraries). > **Info** > > Experience configurations that are nested can cause unexpected behavior in the SDK. For more information, see the [Troubleshooting](/personalization/troubleshooting#experience-configurations-are-nested) guide. ### `` component props | Prop | Required | Description | | :----------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `{...baseline}` | Yes | Any and all props that the function passed as the `component` prop needs to receive to render the baseline variant entry. This will depend entirely on the structure of your existing React component(s). | | `id` | Yes | The CMS entry ID of the baseline. | | `component` | Yes | The React component that your baseline and variants will use to render. This can either be regular React component or a component that opts into React's `forwardRef`. | | `experiences` | Yes | An array of experience CMS entries mapped using the ExperienceMapper methods available from our utility libraries. | | `passthroughProps` | No | An object containing key-value pairs of props that should be sent to the `component` irrespective of which experience variant is selected. Props supplied here will overwrite those of the selected variant, so this is designed for non-content props like state or refs. | | `loadingComponent` | No | A custom component to show prior to the `Experience` component selecting a variant. This defaults to a transparent version of your component using the baseline props. | | `trackClicks` | No | A boolean to determine whether clicks on interactive elements inside of the rendered element should send `component_click` events | | `trackHovers` | No | A boolean to determine whether hovers inside of the rendered element should send `component_hover` events | #### Example use These examples show working CMS data, our Utility Libraries, and the `` component together in demonstrative examples. Your implementation will vary according to your existing React components and your data source. Consult the [Utility libraries](#utility-libraries) documentation to know what data to fetch from your content source and how to transform the returned Experience entries to the format required by the `` component. These examples show fetching CMS data from within potentially deeply nested React components. In practice, you will likely fetch that data from higher within your rendering tree and pass it to components, especially when statically pre-rendering. However, the mapping exercises and use of the `` component demonstrated remain the same no matter what rendering strategy you adopt. #### General JavaScript Utils Library ```jsx // or '@ninetailed/experience.js-next' import { Experience } from '@ninetailed/experience.js-react'; import { ExperienceMapper } from '@ninetailed/experience.js-utils' // This function is assumed to return a single entry and all its supporting data, including referenced content, in their entirety import { getCmsEntry } from '../api/yourEntryGetter' import { YourComponent } from './YourComponent' export const YourExperience = (cmsBaselineEntry) => { const baselineEntry = getCmsEntry(cmsBaselineEntry); const experiences = baselineEntry['nt_experiences'] const mappedExperiences = (experiences || []) .map((experience) => { return { id: experience.id, name: experience.name type: experience.nt_type as 'nt_personalization' | 'nt_experiment' config: experience.nt_config, audience: { id: experience.nt_audience.nt_audience_id }, variants: experience.variants.map((variant) => { return { id: variant.id, // Required // Map any other data from the variant required by your component ...variant, someComponentProp: variant.foo } }) } }) .filter((experience) => ExperienceMapper.isExperienceEntry(experience)) .map((experience) => ExperienceMapper.mapExperience(experience)); return ( ); }; ``` #### Contentful Utils Library ```jsx // or '@ninetailed/experience.js-next' import { Experience } from '@ninetailed/experience.js-react'; // For use with Contentful REST APIs only import { ExperienceMapper } from '@ninetailed/experience.js-utils-contentful' // This function is assumed to return a single entry and all its nested references from the REST Contentful CDA in their entirety import { getContentfulEntry } from '../api/yourEntryGetter' import { YourComponent } from './YourComponent' export const YourExperience = (cmsBaselineEntry) => { const baselineEntry = getContentfulEntry(cmsBaselineEntry); const experiences = baselineEntry.fields.nt_experiences; const mappedExperiences = experiences .filter((experience) => ExperienceMapper.isExperienceEntry(experience)) .map((experience) => ExperienceMapper.mapExperience(experience)) return ( ); }; ``` ### Inline Personalization with merge tags Contentful Personalization allows you to embed content placeholders into Rich Text Fields that can then be rendered client-side using information from the current visitor's profile. These dynamic placeholder entries are called Merge Tags, which can then be used as inline entries within a rich text field of your CMS entries. The React-based SDKs provide a corresponding `` component that allow you to declaratively render the inlined Merge Tag entries. While rendering Merge Tag entries embedded within Rich Text Fields is the most common use for merge tags, you simply pass the property accessor (using dot notation) of any Contentful Personalization profile property as the `id` of the `MergeTag` component. #### From Contentful Entry ```jsx import React from 'react'; import { INLINES } from '@contentful/rich-text-types'; import { documentToReactComponents} from '@contentful/rich-text-react-renderer'; // or `@ninetailed/experience.js-next' import { MergeTag } from '@ninetailed/experience.js-react'; export const renderRichText = (richTextDocument) => { return documentToReactComponents(richTextDocument, { renderNode: { [INLINES.EMBEDDED_ENTRY]: (node) => { if (node.data.target.sys.contentType.sys.id === 'nt_mergetag') { return ( ); } } }, }); }; ``` #### Inline accessor ```jsx import React from 'react'; // or `@ninetailed/experience.js-next' import { MergeTag } from '@ninetailed/experience.js-react'; const Greeting = () => { return ( <>

Welcome back,

How is this time of year?

); }; ``` ### Tracking impressions of Experiences The `` component needs to track when the markup it renders is present within the visitor's viewport. This is the criteria used to fire impression events to any connected Contentful Personalization plugins. Unless the `component` prop being passed to `` is defined as a React `forwardRef`, the `` component will insert an empty non-displaying `
` of class `nt-cmp-marker` immediately prior to the rendered `component`. It is this inserted element's intersection with the viewport that then tracked. ```html // Returned markup from the component when passing a regular component
``` Under the hood, the tracking component uses React's `useRef` to store the DOM node to track. See the React [`forwardRef`](https://react.dev/reference/react/forwardRef) documentation for more details. Because of the use of `useRef`, it is important that the `component`s you pass have some consistent parent element between re-renders. Conditionally rendering top-level elements in a `component` may cause tracking to become decoupled. #### What not to do ```jsx const SomeAsyncConditionalComponent = () => { const [loading, setLoading] = useState(true); useEffect(() => { // ... Do something like get async data setLoading(false); }, []); // This component conditionally renders a top-level element, so tracking might be lost return loading ? (
Some loading markup
) : (
Some markup after loading
); }; const YourExperience = (cmsBaselineEntry) => { // ... Filter and map as above return ( ); }; ``` #### What to do instead ```jsx const SomeAsyncConditionalComponent = () => { const [loading, setLoading] = useState(true); useEffect(() => { // ... Do something like get async data setLoading(false); }, []); // Wrapping an element around the conditional rendering to preserve tracking return (
(loading ? (
Some loading markup
) : (
Some markup after loading
) )
); }; const YourExperience = (cmsBaselineEntry) => { // ... Filter and map as above return ( ); }; ``` > Provide Experiences within your Javascript-based front-end.