Architecture and how it works
This guide explains how Contentful Personalization works at a technical level. Understanding the architecture will help you make better implementation decisions and avoid common setup issues.
How personalization works
Contentful Personalization delivers personalized content by wrapping your existing components with an <Experience> component in React. This wrapper checks which audiences the current visitor belongs to and swaps the baseline content for the matching variant. Your component receives different Contentful entry data, but the component itself does not change.
Content types
The Contentful Personalization app installs three content types in your space:
1. nt_experience
An experience defines a personalization rule or experiment:
- Audience: Which visitors to target (a reference to an
nt_audienceentry). - Variants: Alternative content entries or custom flag change types to display. The
nt_variantsfield is a collection of all possible variants that an Optimization baseline has. - Distribution: The traffic split between variants (for example, 50/50 for an A/B test).
- Type: Either
nt_personalization(deterministic, targets a specific audience) ornt_experiment(random split for A/B testing).
2. nt_audience
An audience defines the targeting rules that determine which visitors qualify. Audiences are evaluated server-side by the Experience API based on visitor attributes such as location, traits, behavior, and session data. You create and manage audience rules in the Contentful web app.
3. nt_mergetag
A merge tag maps a display name to a path in the visitor’s profile. For example, a merge tag named “City” might resolve to location.city, or “First Name” to traits.firstName. Content editors embed merge tag entries in rich text fields for inline personalization. Developers can also use the <MergeTag> component directly in JSX (JavaScript XML).
The nt_experiences field
When you configure a content type for personalization, the app adds an nt_experiences field which is an array of references to nt_experience entries. This field links your content to the experiences that can personalize it.
For example, a Hero content type with an nt_experiences field can have one experience that shows a different headline to visitors from Germany, and another that runs an A/B test on the call-to-action text.
The rendering flow
The typical flow from Contentful content to a personalized component looks like this:
- Fetch entries from the Contentful API with an include depth of at least 2 (see why include depth matters below).
- Extract and map experiences from each entry using the utility library:
- Filter with
ExperienceMapper.isExperienceEntry() - Map with
ExperienceMapper.mapExperience()
- Filter with
- Wrap the component with
<Experience>, passing the mapped experiences and the entry’s fields. - The SDK resolves which variant to show by checking the visitor’s profile against each experience’s audience and selecting a variant based on the distribution weights. This happens through the Experience API. The component renders with either the variant or the baseline content.
For details on the ExperienceMapper utilities, see the Experience SDK documentation.
Why include depth matters
When Contentful resolves references in API responses, the include parameter controls how many levels deep it goes:
include: 1— resolves the entry’s direct references, butnt_experiencesentries appear as unresolved links.include: 2— resolves experience entries, but variant entries within them may not fully resolve.include: 3or higher — resolves the full chain: entry → experience → variant content.
If the include depth is too low, ExperienceMapper.isExperienceEntry() filters out unresolved entries and personalization silently does nothing. The component always shows the baseline.
NOTE:
We recommend using include: <your_current_include> + 3 or higher. Complex page structures with nested sections often use include: 10.
The component mapper pattern
Most Contentful Personalization implementations use a component mapper. A component mapper is an object that maps Contentful content type IDs to React components. A renderer component then wraps each entry with <Experience> automatically:
This approach centralizes personalization in one place. Every component that flows through the renderer is automatically wrapped with <Experience>, so you don’t have to add wrappers manually in each page template.
Client-side vs. server-side personalization
Client-side (default)
With client-side personalization, the server sends static HTML with baseline content. After the page loads, the SDK contacts the Experience API, resolves audiences, and swaps the components to show the correct variants.
NOTE: There are also client-side implementations where the server returns no baseline at all, and the entire baseline/variant rendering logic happens on the client side.
Client-side personalization is the simplest approach and requires no server or edge infrastructure beyond what you already have.
Server-side and edge (SSR/ESR)
With server-side or edge-side personalization, the server or edge middleware contacts the Experience API before rendering HTML. The visitor receives pre-personalized HTML. There is no flash of default content.
This approach requires additional infrastructure:
- Middleware or an edge worker to call the Experience API
- The
@ninetailed/experience.js-plugin-ssrpackage for profile continuity - Cookie management for the
ntaidprofile identifier - Preflight mode (
?type=preflight) to prevent double-counting events when a client SDK also runs
For implementation details, see Edge and Server Side Rendering.
The provider
NinetailedProvider is a React context provider that initializes the SDK and manages the visitor’s state. It handles:
- Creating the Ninetailed instance with your API key.
- Managing the visitor’s profile (traits, audiences, sessions).
- Communicating with the Experience API to resolve variant selections.
- Providing React hooks (
useProfile,useNinetailed) to child components. - Loading plugins for analytics, preview, SSR, and privacy.
The provider must wrap all components you want to personalize. It is typically placed in pages/_app.tsx for Next.js Pages Router projects, or in a client component wrapper around the root layout for App Router projects.
NOTE:
If your implementation is 100% server-side, do not include NinetailedProvider because a React provider can only run on the client-side.
For provider configuration details, see the React SDK documentation.