Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/main-pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ jobs:

- run: pnpm install --prefer-offline --frozen-lockfile
- run: pnpm build:ci
- run: pnpm size:check
- name: Pack SDK tarballs for implementations
run: |
rm -rf pkgs
Expand Down
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
docs
pnpm-lock.yaml
**/.next/**
**/next-env.d.ts
**/ios/Pods/**
**/ios/build/**
**/android/build/**
Expand Down
36 changes: 36 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,36 @@ High-signal repo-wide commands:
- `pnpm test:unit`
- `pnpm build`
- `pnpm size:check`
- `pnpm size:report`
- `pnpm build:pkgs`
- `pnpm format:check`
- `pnpm docs:generate`

## Bundle budget failures

- When `pnpm size:check` or a package `size:check` fails, do not add package exports, new bundle
entries, new chunks, budget config changes, aliases, or separate build outputs to move bytes
outside the failing budget unless the user explicitly asks for that approach.
- `size:check` can stop at the first failing package or bundle. After a `size:check` failure, run
`pnpm size:report` or the relevant package `size:report` before drawing conclusions about the full
set of bundle-budget failures.
- For bundle-budget failures, the only source changes allowed without user direction are concise,
behavior-preserving reductions to code you added or changed for the task.
- Before attempting any bundle-budget fix, report:
- the exact command
- the failing package or bundle
- the budget, actual size, and delta
- the task-related files changed since the last known passing baseline
- the smallest behavior-preserving concision option, if one is clear
- the specific human direction needed if concision is not obvious

## Failure handling and recovery

- Do not rerun the same failing command unchanged more than once.
- Treat validation failures as diagnostic evidence before treating them as code defects. Do not edit
source, add compatibility shims, or change reference implementation code until you have classified
whether the failure is caused by source behavior, stale generated artifacts, install state,
package packaging, environment setup, or tooling.
- Before retrying, classify the failure into one of these buckets:
- command resolution or PATH
- missing prerequisite or setup
Expand All @@ -136,6 +159,9 @@ High-signal repo-wide commands:
- unknown
- Prefer a small probe before a full rerun. Check the nearest `AGENTS.md`, the target
`package.json`, and any relevant `README.md` or `CONTRIBUTING.md` section before guessing.
- If the classified failure is not in source code, stop making source changes. Use the documented
package or implementation script for that failure class, or report the root cause and smallest
next action if cleanup, approval, or environment repair is needed.
- If a lint or format command fails with findings that the tool can auto-fix, prefer a targeted
fix-enabled rerun over repeated check-only runs, then revalidate once.
- If the shell reports a command as missing:
Expand Down Expand Up @@ -240,6 +266,16 @@ High-signal repo-wide commands:
- Do not use broad cleanup commands such as `pm2 delete all` unless explicitly asked.
- Do not assume full cross-platform E2E is required for every change.

## Context resume audit

- After context compaction, interruption, or a long-running resume, audit before editing: reread the
latest user instruction, inspect `git status`, inspect relevant diffs, and restate the active
constraints in a short commentary update.
- If resumed context is missing, contradictory, or no longer explains why a change exists, stop and
ask or report the uncertainty instead of continuing from memory.
- Keep resumed work scoped to the newest user request. Do not continue older plans unless they are
still required by the latest instruction.

## Preferred workflow

1. Read the root `AGENTS.md`.
Expand Down
8 changes: 8 additions & 0 deletions documentation/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,21 @@ structure, cross-linking, and validation rules.

## Heading and writing style

- Write for human software engineers integrating the SDK into consumer applications. Authored docs
are not internal agent instructions or maintainer runbooks.
- Use sentence case for headings.
- Preserve official product, package, API, component, and hook casing.
- Lead with what the reader is trying to implement.
- Separate SDK responsibilities from application responsibilities.
- State what the SDK does not own when relevant, especially Contentful fetching, consent policy,
identity policy, routing, and rendering.
- Prefer concrete implementation guidance over marketing language.
- When documenting an integration constraint, tell the reader what breaks, what to do instead, and
how to choose a default or fallback for their own integration.
- Explain the consequence behind constraints. Prefer reader-facing phrasing such as "all-locale CDA
responses are incompatible with the resolver because..." over unexplained "do not" rules.
- Use direct imperatives only when they help an engineer avoid a concrete integration bug, security
issue, data leak, or broken runtime behavior. Pair them with the reason or the safer alternative.

## Cross-linking

Expand Down
4 changes: 4 additions & 0 deletions documentation/concepts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Concepts
children:
- ./core-state-management.md
- ./entry-personalization-and-variant-resolution.md
- ./locale-handling-in-the-optimization-sdk-suite.md
- ./interaction-tracking-in-web-sdks.md
- ./interaction-tracking-in-node-and-stateless-environments.md
- ./profile-synchronization-between-client-and-server.md
Expand All @@ -27,6 +28,9 @@ they are not the first stop for installation or setup commands.
- [Entry personalization and variant resolution](./entry-personalization-and-variant-resolution.md) -
explains how the SDK resolves a Contentful baseline entry to the selected entry variant, including
data model expectations, fallback behavior, resolution paths, and preview overrides.
- [Locale handling in the Optimization SDK Suite](./locale-handling-in-the-optimization-sdk-suite.md) -
explains how Contentful locales, `contentful.js` locale response shapes, Experience API locale
options, SDK-assisted locale resolution, and runtime-specific locale sources work together.
- [Interaction tracking in Web SDKs](./interaction-tracking-in-web-sdks.md) - explains how
`@contentful/optimization-web` and `@contentful/optimization-react-web` detect browser entry
views, clicks, hovers, Custom Flag views, page events, and custom events, including consent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,26 @@ title: Entry personalization and variant resolution

Use this document to understand how the Optimization SDK Suite resolves a Contentful baseline entry
to the entry variant selected for a visitor. It explains the runtime contract shared by the Core,
Web, React Web, React Native, and Node SDKs.
Web, React Web, React Native, and Node SDKs. Because rendered entries can also contain MergeTag
entries, it also calls out where profile-dependent MergeTag value resolution differs from entry
replacement.

For installation and package setup, use the relevant integration guide. For state propagation before
resolution, see [Core state management](./core-state-management.md).
resolution, see [Core state management](./core-state-management.md). For a broader explanation of
Contentful, Experience API, and runtime locale handling, see
[Locale handling in the Optimization SDK Suite](./locale-handling-in-the-optimization-sdk-suite.md).

<details>
<summary>Table of Contents</summary>
<!-- mtoc-start -->

- [Resolution boundary](#resolution-boundary)
- [Single-locale CDA entry contract](#single-locale-cda-entry-contract)
- [Data model](#data-model)
- [Baseline entry](#baseline-entry)
- [Optimization entry](#optimization-entry)
- [Selected optimization](#selected-optimization)
- [Merge tags and localized profile values](#merge-tags-and-localized-profile-values)
- [Resolution flow](#resolution-flow)
- [Worked example](#worked-example)
- [Variant indexing](#variant-indexing)
Expand Down Expand Up @@ -60,6 +66,57 @@ Application fetches Contentful baseline entry
-> SDK returns baseline or linked variant entry for rendering
```

## Single-locale CDA entry contract

The resolver expects standard single-locale Contentful Delivery API entry payloads. In that shape,
localized fields are direct field values, so optimization references are available as
`entry.fields.nt_experiences` and variant entries are available as
`optimizationEntry.fields.nt_variants`.

All-locale CDA payloads are incompatible with SDK entry resolution. `contentful.js` `withAllLocales`
and raw CDA `locale=*` return locale-keyed field maps instead of direct values, for example
`fields.nt_experiences['en-US']`. The SDK resolver intentionally handles one localized entry at a
time, so locale-keyed maps do not match the `OptimizedEntry` schema and resolution falls back to the
baseline entry.

Fetch the entry with a single CDA locale and enough include depth for optimization links:

```ts
const optimization = new ContentfulOptimization({
clientId,
environment,
contentfulLocales: {
default: 'en-US',
supported: ['en-US', 'de-DE', 'fr-FR'],
},
})

const contentful = optimization.withOptimizationLocale(contentfulClient)
const baselineEntry = await contentful.getEntry(entryId, {
include: 10,
})
```

Use the SDK-resolved Contentful locale for CDA entry fetches that feed entry resolution. Browser,
React Web, and React Native apps can use `withOptimizationLocale(contentfulClient)` or pass
`optimization.locale` explicitly. Node and SSR apps can use the `contentfulLocale` returned by
`resolveRequestLocale(reqOrAcceptLanguage)`. Native iOS and Android apps can use `client.locale` in
their app-owned CDA request code.

That Contentful locale is separate from event context locale and from an explicit Experience API
`api.locale` override. For the full locale model, including runtime candidates, fallback order,
Experience API localization, and environment-specific surfaces, see
[Locale handling in the Optimization SDK Suite](./locale-handling-in-the-optimization-sdk-suite.md).

For entries that will be passed to the Optimization SDK resolver, use a concrete locale instead of
the CDA wildcard locale:

```ts
// These patterns return all locales and produce locale-keyed fields.
await contentfulClient.withAllLocales.getEntry(entryId, { include: 10 })
await fetch(`/spaces/${space}/environments/${environment}/entries/${entryId}?include=10&locale=*`)
```

## Data model

### Baseline entry
Expand Down Expand Up @@ -103,6 +160,28 @@ resolved through the Custom Flag and `changes` flow, not through `resolveOptimiz
The resolver uses `experienceId` and `variantIndex`. It returns the full `SelectedOptimization` as
metadata only when it resolves to a non-baseline variant.

### Merge tags and localized profile values

Entry rendering can also involve MergeTag entries embedded in Rich Text. MergeTag helpers such as
`getMergeTagValue()` resolve against the current profile data returned by the Experience API. This
is separate from entry replacement: `resolveOptimizedEntry()` chooses the entry variant, while
MergeTag helpers resolve profile-dependent values inside rendered fields.

Stateful SDKs use the current resolved app/content locale as the default Experience API locale
unless an explicit `api.locale` override is provided. Apps can change that resolved locale with
`setLocale()` or, in React providers, by changing the provider `locale` prop. Locale changes update
SDK state only; fetch content or call `page()`, `screen()`, or `identify()` again when the rendered
data needs to refresh. Stateless server code should pass the `contentfulLocale` returned by
`resolveRequestLocale()` as the per-call `{ locale }` request option when that value is present.
That keeps localized profile values, such as `location.city` and `location.country`, in the same
language as the rendered Contentful content. When `contentfulLocale` is absent because no
`contentfulLocales` config is present, omit the request locale option intentionally.

That request locale is not a Contentful CDA locale. The Experience API validates locale tag syntax
and uses its own default when the query parameter is omitted. If the locale tag is invalid, the API
request can fail validation before a profile response is returned. If the tag is valid but a
specific location translation is not available, localized location values may remain untranslated.

## Resolution flow

The shared Core resolver follows one path for every SDK package:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,18 @@ Server-side `page()` is the normal SSR entry point. It records the page request,
`OptimizationData`, and gives the server `profile`, `selectedOptimizations`, and `changes` for the
render.

Event `context.locale` is event data. It can be used by analytics and audience rules that inspect
event context, but it does not choose the CDA locale for Contentful entry fetches and it is separate
from the Experience API request `locale` query parameter. For the broader locale model, see
[Locale handling in the Optimization SDK Suite](./locale-handling-in-the-optimization-sdk-suite.md).

```ts
const { contentfulLocale, eventLocale } = optimization.resolveRequestLocale(req)
const requestOptions = contentfulLocale ? { locale: contentfulLocale } : undefined

const pageResponse = await optimization.page(
{
locale: eventLocale,
profile: profileId ? { id: profileId } : undefined,
page: {
path: req.path,
Expand All @@ -136,16 +145,20 @@ const pageResponse = await optimization.page(
},
userAgent: req.get('user-agent') ?? 'node-server',
},
{ locale: req.acceptsLanguages()[0] ?? 'en-US' },
requestOptions,
)
```

Use server-side `track()` for server-known business events:

```ts
const { contentfulLocale, eventLocale } = optimization.resolveRequestLocale(req)
const requestOptions = contentfulLocale ? { locale: contentfulLocale } : undefined

await optimization.track(
{
profile: pageResponse.profile,
locale: eventLocale,
event: 'quote_requested',
properties: {
plan: 'enterprise',
Expand All @@ -163,11 +176,16 @@ visibility, use browser tracking.
```ts
import { randomUUID } from 'node:crypto'

const { contentfulLocale, eventLocale } = optimization.resolveRequestLocale(req)
const requestOptions = contentfulLocale ? { locale: contentfulLocale } : undefined

await optimization.trackView(
{
profile: pageResponse.profile,
locale: eventLocale,
componentId: resolvedEntry.sys.id,
experienceId: selectedOptimization?.experienceId,
sticky: selectedOptimization?.sticky ?? false,
variantIndex: selectedOptimization?.variantIndex,
viewDurationMs: 0,
viewId: randomUUID(),
Expand Down
Loading
Loading