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_audience entry).
  • Variants: Alternative content entries or custom flag change types to display. The nt_variants field 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) or nt_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:

  1. Fetch entries from the Contentful API with an include depth of at least 2 (see why include depth matters below).
  2. Extract and map experiences from each entry using the utility library:
    • Filter with ExperienceMapper.isExperienceEntry()
    • Map with ExperienceMapper.mapExperience()
  3. Wrap the component with <Experience>, passing the mapped experiences and the entry’s fields.
  4. 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, but nt_experiences entries appear as unresolved links.
  • include: 2 — resolves experience entries, but variant entries within them may not fully resolve.
  • include: 3 or 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:

1const ContentTypeMap = {
2 hero: Hero,
3 cta: CTA,
4 feature: Feature,
5 banner: Banner,
6};
7
8const BlockRenderer = ({ block }) => {
9 const contentTypeId = block.sys.contentType.sys.id;
10 const Component = ContentTypeMap[contentTypeId];
11 if (!Component) return null;
12
13 const experiences = (block.fields.nt_experiences || [])
14 .filter(ExperienceMapper.isExperienceEntry)
15 .map(ExperienceMapper.mapExperience);
16
17 return (
18 <Experience
19 {...block}
20 id={block.sys.id}
21 component={ComponentRenderer}
22 experiences={experiences}
23 />
24 );
25};

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-ssr package for profile continuity
  • Cookie management for the ntaid profile 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.