@contentful/optimization-node 0.1.0-alpha9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <p align="center">
2
2
  <a href="https://www.contentful.com/developers/docs/personalization/">
3
- <img alt="Contentful Logo" title="Contentful" src="../../../contentful-icon.png" width="150">
3
+ <img alt="Contentful Logo" title="Contentful" src="https://raw.githubusercontent.com/contentful/optimization/optimization-node-v1.0.0/contentful-icon.png" width="150">
4
4
  </a>
5
5
  </p>
6
6
 
@@ -10,314 +10,311 @@
10
10
 
11
11
  <div align="center">
12
12
 
13
- [Readme](./README.md) · [Reference](https://contentful.github.io/optimization) ·
14
- [Contributing](../../../CONTRIBUTING.md)
13
+ [Guides](https://contentful.github.io/optimization/documents/Documentation.Guides.html) ·
14
+ [Reference](https://contentful.github.io/optimization) · [Contributing](https://github.com/contentful/optimization/blob/optimization-node-v1.0.0/CONTRIBUTING.md)
15
15
 
16
16
  </div>
17
17
 
18
18
  > [!WARNING]
19
19
  >
20
- > The Optimization SDK Suite is pre-release (alpha). Breaking changes may be published at any time.
20
+ > The Optimization SDK Suite is pre-release (alpha). Breaking changes can be published at any time.
21
21
 
22
- The Optimization Node SDK implements functionality specific to Node environments, based on the
23
- [Optimization Core Library](../../../universal/core/README.md). This SDK is part of the
24
- [Contentful Optimization SDK Suite](../../../README.md).
22
+ The Optimization Node SDK implements stateless server-side optimization behavior on top of the
23
+ [Optimization Core SDK](https://github.com/contentful/optimization/blob/optimization-node-v1.0.0/packages/universal/core-sdk/README.md). Use it for server rendering, server
24
+ functions, and Node services that need request-scoped profile evaluation or event emission.
25
+
26
+ If you are integrating a Node application, start with [Getting Started](#getting-started), then use
27
+ [Integrating the Optimization Node SDK in a Node app](https://contentful.github.io/optimization/documents/Documentation.Guides.integrating-the-node-sdk-in-a-node-app.html)
28
+ for the step-by-step flow. This README keeps the package orientation and common setup options close
29
+ at hand; generated [reference documentation](https://contentful.github.io/optimization) remains the
30
+ source of truth for exported API signatures.
25
31
 
26
32
  <details>
27
33
  <summary>Table of Contents</summary>
28
34
  <!-- mtoc-start -->
29
35
 
30
- - [Getting Started](#getting-started)
31
- - [Reference Implementations](#reference-implementations)
32
- - [Configuration](#configuration)
33
- - [Top-level Configuration Options](#top-level-configuration-options)
34
- - [Analytics Options](#analytics-options)
35
- - [Event Builder Options](#event-builder-options)
36
- - [Fetch Options](#fetch-options)
37
- - [Personalization Options](#personalization-options)
38
- - [Optimization Methods](#optimization-methods)
39
- - [Personalization Data Resolution Methods](#personalization-data-resolution-methods)
40
- - [`getCustomFlag`](#getcustomflag)
41
- - [`personalizeEntry`](#personalizeentry)
42
- - [`getMergeTagValue`](#getmergetagvalue)
43
- - [Personalization and Analytics Event Methods](#personalization-and-analytics-event-methods)
44
- - [`identify`](#identify)
45
- - [`page`](#page)
46
- - [`screen`](#screen)
47
- - [`track`](#track)
48
- - [`trackComponentView`](#trackcomponentview)
49
- - [`trackComponentClick`](#trackcomponentclick)
50
- - [`trackFlagView`](#trackflagview)
51
- - [Interceptors](#interceptors)
52
- - [Life-cycle Interceptors](#life-cycle-interceptors)
36
+ - [Getting started](#getting-started)
37
+ - [When to use this package](#when-to-use-this-package)
38
+ - [Common configuration](#common-configuration)
39
+ - [Core workflows](#core-workflows)
40
+ - [Request-scoped events](#request-scoped-events)
41
+ - [Content resolution](#content-resolution)
42
+ - [Caching guidance](#caching-guidance)
43
+ - [Development harness](#development-harness)
44
+ - [Related](#related)
53
45
 
54
46
  <!-- mtoc-end -->
55
47
  </details>
56
48
 
57
- ## Getting Started
49
+ ## Getting started
58
50
 
59
51
  Install using an NPM-compatible package manager, pnpm for example:
60
52
 
61
53
  ```sh
62
- pnpm install @contentful/optimization-node
54
+ pnpm add @contentful/optimization-node
63
55
  ```
64
56
 
57
+ Add `contentful` too when the SDK will use your app-owned `contentful.js` client for managed entry
58
+ fetching.
59
+
65
60
  Import the Optimization class; both CJS and ESM module systems are supported, ESM preferred:
66
61
 
67
62
  ```ts
68
- import Optimization from '@contentful/optimization-node'
63
+ import ContentfulOptimization from '@contentful/optimization-node'
69
64
  ```
70
65
 
71
- Configure and initialize the Optimization Node SDK:
66
+ Create the SDK once per module or process, then bind consent and request context in each route:
72
67
 
73
68
  ```ts
74
- const optimization = new Optimization({ clientId: 'abc123' })
75
- ```
76
-
77
- ## Reference Implementations
78
-
79
- Reference implementations illustrate how the SDK may be used under common scenarios, as well as
80
- select less-common scenarios, with the most basic example solution possible.
81
-
82
- - [Node SSR Only](../../../implementations/node-ssr-only/README.md): Example application that uses
83
- the Node SDK to render a personalized Web page
84
- - [Node SSR + Web Vanilla](../../../implementations/node-ssr-web-vanilla/README.md): Example
85
- application demonstrating simple profile synchronization between the Node and
86
- [Web](../web/README.md) SDKs via cookie
87
-
88
- ## Configuration
89
-
90
- ### Top-level Configuration Options
91
-
92
- | Option | Required? | Default | Description |
93
- | ----------------- | --------- | ----------------------------- | ------------------------------------------------------------------------------ |
94
- | `analytics` | No | See "Analytics Options" | Configuration specific to the Analytics/Insights API |
95
- | `app` | No | `undefined` | The application definition used to attribute events to a specific consumer app |
96
- | `clientId` | Yes | N/A | The Optimization API key |
97
- | `environment` | No | `'main'` | The environment identifier |
98
- | `eventBuilder` | No | See "Event Builder Options" | Event builder configuration (channel/library metadata, etc.) |
99
- | `fetchOptions` | No | See "Fetch Options" | Configuration for Fetch timeout and retry functionality |
100
- | `logLevel` | No | `'error'` | Minimum log level for the default console sink |
101
- | `personalization` | No | See "Personalization Options" | Configuration specific to the Personalization/Experience API |
102
-
103
- ### Analytics Options
104
-
105
- | Option | Required? | Default | Description |
106
- | --------- | --------- | ------------------------------------------ | ----------------------------- |
107
- | `baseUrl` | No | `'https://ingest.insights.ninetailed.co/'` | Base URL for the Insights API |
108
-
109
- ### Event Builder Options
110
-
111
- Event builder options should only be supplied when building an SDK on top of the Optimization Node
112
- SDK or any of its descendent SDKs.
113
-
114
- | Option | Required? | Default | Description |
115
- | --------- | --------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------- |
116
- | `channel` | No | `'server'` | The channel that identifies where events originate from (e.g. `'web'`, `'mobile'`) |
117
- | `library` | No | `{ name: 'Optimization Node API', version: '0.0.0' }` | The client library metadata that is attached to all events |
118
-
119
- ### Fetch Options
120
-
121
- Fetch options allow for configuration of a Fetch API-compatible fetch method and the retry/timeout
122
- logic integrated into the Optimization API Client. Specify the `fetchMethod` when the host
123
- application environment does not offer a `fetch` method that is compatible with the standard Fetch
124
- API in its global scope.
125
-
126
- | Option | Required? | Default | Description |
127
- | ------------------ | --------- | ----------- | --------------------------------------------------------------------- |
128
- | `fetchMethod` | No | `undefined` | Signature of a fetch method used by the API clients |
129
- | `intervalTimeout` | No | `0` | Delay (in milliseconds) between retry attempts |
130
- | `onFailedAttempt` | No | `undefined` | Callback invoked whenever a retry attempt fails |
131
- | `onRequestTimeout` | No | `undefined` | Callback invoked when a request exceeds the configured timeout |
132
- | `requestTimeout` | No | `3000` | Maximum time (in milliseconds) to wait for a response before aborting |
133
- | `retries` | No | `1` | Maximum number of retry attempts |
134
-
135
- Configuration method signatures:
136
-
137
- - `fetchMethod`: `(url: string | URL, init: RequestInit) => Promise<Response>`
138
- - `onFailedAttempt` and `onRequestTimeout`: `(options: FetchMethodCallbackOptions) => void`
139
-
140
- ### Personalization Options
141
-
142
- | Option | Required? | Default | Description |
143
- | ----------------- | --------- | ------------------------------------- | ------------------------------------------------------------------- |
144
- | `baseUrl` | No | `'https://experience.ninetailed.co/'` | Base URL for the Experience API |
145
- | `enabledFeatures` | No | `['ip-enrichment', 'location']` | Enabled features which the API may use for each request |
146
- | `ip` | No | `undefined` | IP address to override the API behavior for IP analysis |
147
- | `locale` | No | `'en-US'` (in API) | Locale used to translate `location.city` and `location.country` |
148
- | `plainText` | No | `false` | Sends performance-critical endpoints in plain text |
149
- | `preflight` | No | `false` | Instructs the API to aggregate a new profile state but not store it |
150
-
151
- ## Optimization Methods
152
-
153
- Arguments marked with an asterisk (\*) are always required.
154
-
155
- ### Personalization Data Resolution Methods
156
-
157
- #### `getCustomFlag`
158
-
159
- Get the specified Custom Flag's value from the provided changes array.
160
-
161
- Arguments:
162
-
163
- - `name`\*: The name/key of the Custom Flag
164
- - `changes`: Changes array
165
-
166
- Returns:
167
-
168
- - The resolved value for the specified Custom Flag, or `undefined` if it cannot be found.
169
-
170
- > [!NOTE]
171
- >
172
- > If the `changes` argument is omitted, the method will return `undefined`.
173
-
174
- #### `personalizeEntry`
175
-
176
- Resolve a baseline Contentful entry to a personalized variant using the provided selected
177
- personalizations.
178
-
179
- Type arguments:
180
-
181
- - `S`: Entry skeleton type
182
- - `M`: Chain modifiers
183
- - `L`: Locale code
184
-
185
- Arguments:
186
-
187
- - `entry`\*: The entry to personalize
188
- - `personalizations`: Selected personalizations
189
-
190
- Returns:
191
-
192
- - The resolved personalized entry variant, or the supplied baseline entry if baseline is the
193
- selected variant or a variant cannot be found.
194
-
195
- > [!NOTE]
196
- >
197
- > If the `personalizations` argument is omitted, the method will return the baseline entry.
198
-
199
- #### `getMergeTagValue`
200
-
201
- Resolve a "Merge Tag" to a value based on the provided profile. A "Merge Tag" is a special Rich Text
202
- fragment supported by Contentful that specifies a profile data member to be injected into the Rich
203
- Text when rendered.
204
-
205
- Arguments:
206
-
207
- - `embeddedNodeEntryTarget`\*: The merge tag entry node to resolve
208
- - `profile`: The user profile
209
-
210
- > [!NOTE]
211
- >
212
- > If the `profile` argument is omitted, the method will return the merge tag's fallback value.
213
-
214
- ### Personalization and Analytics Event Methods
215
-
216
- Only the following methods may return an `OptimizationData` object:
217
-
218
- - `identify`
219
- - `page`
220
- - `screen`
221
- - `track`
222
- - `trackComponentView` (when `payload.sticky` is `true`)
223
-
224
- `trackComponentClick` and `trackFlagView` return no data. When returned, `OptimizationData`
225
- contains:
226
-
227
- - `changes`: Currently used for Custom Flags
228
- - `personalizations`: Selected personalizations for the profile
229
- - `profile`: Profile associated with the evaluated events
230
-
231
- #### `identify`
232
-
233
- Identify the current profile/visitor to associate traits with a profile.
234
-
235
- Arguments:
236
-
237
- - `payload`\*: Identify event builder arguments object, including an optional `profile` property
238
- with a `PartialProfile` value that requires only an `id`
239
-
240
- #### `page`
241
-
242
- Record a personalization page view.
243
-
244
- Arguments:
69
+ const optimization = new ContentfulOptimization({
70
+ clientId: 'your-client-id',
71
+ environment: 'main',
72
+ locale: 'en-US',
73
+ })
245
74
 
246
- - `payload`\*: Page view event builder arguments object, including an optional `profile` property
247
- with a `PartialProfile` value that requires only an `id`
75
+ function appPolicyAllowsOptimizationEvent(req: { cookies?: Record<string, string> }): boolean {
76
+ return req.cookies?.['app-personalization-consent'] === 'granted'
77
+ }
78
+
79
+ async function renderRequest(
80
+ req: { cookies?: Record<string, string>; headers: { 'accept-language'?: string } },
81
+ profileId: string,
82
+ ) {
83
+ const appLocale = getAppLocale(req)
84
+ const requestOptimization = optimization.forRequest({
85
+ consent: {
86
+ events: appPolicyAllowsOptimizationEvent(req),
87
+ persistence: appPolicyAllowsOptimizationEvent(req),
88
+ },
89
+ locale: appLocale,
90
+ eventContext: { locale: appLocale },
91
+ profile: { id: profileId },
92
+ })
93
+
94
+ const { accepted, data } = await requestOptimization.page()
95
+
96
+ return accepted ? data : undefined
97
+ }
98
+ ```
248
99
 
249
- #### `screen`
100
+ ## When to use this package
250
101
 
251
- Record a personalization screen view.
102
+ Use `@contentful/optimization-node` for server-side rendering, server functions, and Node services
103
+ that need stateless profile evaluation or event emission. Use the Web or React Web SDK alongside it
104
+ when browser-side consent, anonymous ID persistence, automatic interaction tracking, or live updates
105
+ are part of the same application.
252
106
 
253
- Arguments:
107
+ ## Common configuration
254
108
 
255
- - `payload`\*: Screen view event builder arguments object, including an optional `profile` property
256
- with a `PartialProfile` value that requires only an `id`
109
+ The Node SDK is stateless. It does not maintain consent, profile, or browser persistence state
110
+ between requests. For cross-SDK consent guidance, see
111
+ [Consent management in the Optimization SDK Suite](https://github.com/contentful/optimization/blob/optimization-node-v1.0.0/documentation/concepts/consent-management-in-the-optimization-sdk-suite.md).
257
112
 
258
- #### `track`
113
+ | Option | Required? | Default | Description |
114
+ | ------------------- | --------- | ---------------------- | ----------------------------------------------------------- |
115
+ | `clientId` | Yes | N/A | Shared API key for Experience API and Insights API requests |
116
+ | `environment` | No | `'main'` | Contentful environment identifier |
117
+ | `api` | No | See API options below | Experience API and Insights API endpoint options |
118
+ | `app` | No | `undefined` | Application metadata attached to outgoing event context |
119
+ | `contentful` | No | `undefined` | App-owned `contentful.js` client, default query, and cache |
120
+ | `locale` | No | `undefined` | Default SDK Experience API and event locale |
121
+ | `fetchOptions` | No | SDK defaults | Fetch timeout and retry behavior |
122
+ | `allowedEventTypes` | No | `['identify', 'page']` | Event types allowed before request event consent is granted |
123
+ | `eventBuilder` | No | Node SDK defaults | Event metadata overrides for SDK-layer authors |
124
+ | `logLevel` | No | `'error'` | Minimum log level for the default console sink |
259
125
 
260
- Record a personalization custom track event.
126
+ Common `api` options:
261
127
 
262
- Arguments:
128
+ | Option | Required? | Default | Description |
129
+ | ------------------- | --------- | ------------------------------------------ | ------------------------------------------------ |
130
+ | `experienceBaseUrl` | No | `'https://experience.ninetailed.co/'` | Base URL for the Experience API |
131
+ | `insightsBaseUrl` | No | `'https://ingest.insights.ninetailed.co/'` | Base URL for the Insights API |
132
+ | `enabledFeatures` | No | `['ip-enrichment', 'location']` | Experience API features to apply to each request |
263
133
 
264
- - `payload`\*: Track event builder arguments object, including an optional `profile` property with a
265
- `PartialProfile` value that requires only an `id`
134
+ Request-scoped Experience options belong in `experienceOptions` when creating the request-bound
135
+ client:
266
136
 
267
- #### `trackComponentView`
137
+ | Option | Description |
138
+ | ----------- | ------------------------------------------------------------- |
139
+ | `ip` | IP address override used by the Experience API |
140
+ | `locale` | Locale query parameter for localized Experience API responses |
141
+ | `plainText` | Sends performance-critical Experience API endpoints as text |
142
+ | `preflight` | Aggregates a new profile state without storing it |
268
143
 
269
- Record an analytics component view event. When the payload marks the component as "sticky", an
270
- additional personalization component view is recorded. This method only returns `OptimizationData`
271
- when the component is marked as "sticky".
144
+ Request-scoped Insights options belong in `insightsOptions`:
272
145
 
273
- Arguments:
146
+ | Option | Description |
147
+ | -------- | ----------------------------------------------------------------- |
148
+ | `beacon` | Last-chance sender for serialized Insights API batches if needed. |
274
149
 
275
- - `payload`\*: Component view event builder arguments object, including an optional `profile`
276
- property with a `PartialProfile` value that requires only an `id`
150
+ Use the request-scoped top-level `locale` on `forRequest()` as the promoted path for localized
151
+ Experience API responses and default event context. `experienceOptions.locale` remains available as
152
+ an advanced low-level pass-through when `locale` is not supplied. If both are provided, request
153
+ `locale` wins.
277
154
 
278
- #### `trackComponentClick`
155
+ Common `fetchOptions` are `fetchMethod`, `requestTimeout`, `retries`, `intervalTimeout`,
156
+ `onFailedAttempt`, and `onRequestTimeout`. Default retries intentionally apply only to HTTP `503`
157
+ responses.
279
158
 
280
- Record an analytics component click event.
159
+ Choose the application Contentful locale in your router, i18n layer, or request policy. Pass that
160
+ value directly to Contentful CDA requests, and pass the same value to
161
+ `forRequest({ locale: appLocale })` when Experience API responses and event context should use the
162
+ same language. Merge tags that reference localized profile fields such as `location.city` and
163
+ `location.country` then resolve in a language consistent with the rendered content. See
164
+ [Locale handling in the Optimization SDK Suite](https://contentful.github.io/optimization/documents/Documentation.Concepts.Locale_handling_in_the_Optimization_SDK_Suite.html)
165
+ for the full locale model.
281
166
 
282
- Returns:
167
+ For every option, callback payload, and exported type, use the generated
168
+ [Node SDK reference](https://contentful.github.io/optimization/modules/_contentful_optimization-node.html).
283
169
 
284
- - `void`
170
+ ## Core workflows
285
171
 
286
- Arguments:
172
+ ### Request-scoped events
287
173
 
288
- - `payload`\*: Component click event builder arguments object
174
+ Build event context from the incoming request, bind application-owned consent state with
175
+ `forRequest()`, then call `page()`, `identify()`, `screen()`, `track()`, or sticky `trackView()` on
176
+ the returned request object:
289
177
 
290
- #### `trackFlagView`
178
+ ```ts
179
+ import type { Request } from 'express'
180
+
181
+ app.get('/products/:slug', async (req, res) => {
182
+ const appLocale = getAppLocale(req)
183
+ const requestOptimization = optimization.forRequest({
184
+ consent: {
185
+ events: appPolicyAllowsOptimizationEvent(req),
186
+ persistence: appPolicyAllowsOptimizationEvent(req),
187
+ },
188
+ locale: appLocale,
189
+ eventContext: {
190
+ locale: appLocale,
191
+ },
192
+ profile: { id: req.cookies.profileId },
193
+ })
194
+ const { accepted, data: optimizationData } = await requestOptimization.page({
195
+ properties: { path: req.path },
196
+ })
197
+
198
+ if (accepted && requestOptimization.canPersistProfile && optimizationData?.profile.id) {
199
+ persistProfileId(res, optimizationData.profile.id)
200
+ }
201
+
202
+ res.render('product', { optimizationData })
203
+ })
204
+ ```
291
205
 
292
- Track a feature flag view via analytics. This is functionally the same as a non-sticky component
293
- view event.
206
+ For default-on application policies that do not render an end-user consent UI, replace the
207
+ request-specific consent lookup with accepted consent:
294
208
 
295
- Returns:
209
+ ```ts
210
+ const requestOptimization = optimization.forRequest({
211
+ consent: { events: true, persistence: true },
212
+ locale: appLocale,
213
+ eventContext: { locale: appLocale },
214
+ profile,
215
+ })
216
+ ```
296
217
 
297
- - `void`
218
+ Node SDK event calls fail closed except for the default pre-consent allowlist, `identify` and
219
+ `page`. Those allowlisted events are sent with `context.gdpr.isConsentGiven: false` when request
220
+ event consent is not granted. Pass `allowedEventTypes: []` to require strict opt-in for all
221
+ stateless event methods.
298
222
 
299
- Arguments:
223
+ In stateless runtimes, Insights-backed methods require a request-bound profile for delivery.
224
+ Non-sticky `trackView`, `trackClick`, `trackHover`, and `trackFlagView` require a profile ID passed
225
+ to `forRequest()`.
300
226
 
301
- - `payload`\*: Component view event builder arguments object
227
+ ### Content resolution
302
228
 
303
- ## Interceptors
229
+ When a `contentful.js` client is available, prefer SDK-managed fetching by entry ID. Configure the
230
+ client once, then call `fetchOptimizedEntry()` on a request-bound client after an accepted
231
+ Experience API call. `forRequest()` clients use the latest selected optimizations when omitted;
232
+ singleton calls require explicit `selectedOptimizations`.
304
233
 
305
- Interceptors may be used to read and/or modify data flowing through the Core SDK.
234
+ ```ts
235
+ const optimization = new ContentfulOptimization({
236
+ clientId: 'client-id',
237
+ contentful: { client: contentfulClient },
238
+ environment: 'main',
239
+ locale: appLocale,
240
+ })
306
241
 
307
- ### Life-cycle Interceptors
242
+ const requestOptimization = optimization.forRequest({ consent: true, profile })
243
+ await requestOptimization.page()
244
+ const { baselineEntry, entry } = await requestOptimization.fetchOptimizedEntry('hero-entry')
245
+ ```
308
246
 
309
- - `event`: Intercepts an event's data _before_ it is queued and/or emitted
310
- - `state`: Intercepts state data retrieved from an Experience API call _before_ updating the SDK's
311
- internal state
247
+ Use `fetchContentfulEntries()` or `prefetchManagedEntries()` when a route knows several managed
248
+ entry IDs. Entries with the same normalized query share one `getEntries()` call; same-tick
249
+ single-entry calls can join that batch. Large `getEntries()` fetches are split into 100-ID chunks.
312
250
 
313
- Example interceptor usage:
251
+ If your application already fetched the baseline entry, keep using the manual resolver:
314
252
 
315
253
  ```ts
316
- optimization.interceptors.event((event) => {
317
- event.properties.timestamp = new Date().toISOString()
318
- })
254
+ const pageResult = await requestOptimization.page()
255
+ const resolvedEntry = optimization.resolveOptimizedEntry(
256
+ baselineEntry,
257
+ pageResult.accepted ? pageResult.data.selectedOptimizations : undefined,
258
+ )
319
259
  ```
320
260
 
321
- > [!WARNING]
322
- >
323
- > Interceptors are intended to enable low-level interoperability and should be used with care.
261
+ Use one CDA locale in either path. For localized apps, configure your application locale. A
262
+ request-bound client uses `forRequest({ locale: appLocale })` as the managed Contentful query locale
263
+ when neither `contentful.defaultQuery` nor the per-call query sets `locale`. Pass the same value to
264
+ app-owned Contentful CDA requests and to `forRequest({ locale: appLocale })` when MergeTags that
265
+ read localized profile fields match the rendered entry language. Do not pass all-locale CDA
266
+ responses from `withAllLocales` or `locale=*`; the resolver expects direct single-locale field
267
+ values. See
268
+ [Entry personalization and variant resolution](https://contentful.github.io/optimization/documents/Documentation.Concepts.Entry_personalization_and_variant_resolution.html#single-locale-cda-entry-contract)
269
+ for the entry contract and
270
+ [Locale handling in the Optimization SDK Suite](https://contentful.github.io/optimization/documents/Documentation.Concepts.Locale_handling_in_the_Optimization_SDK_Suite.html)
271
+ for request locale behavior.
272
+
273
+ Use `getMergeTagValue()` for Contentful Rich Text merge tags and `getFlag()` for Custom Flags. If a
274
+ merge tag references localized profile fields such as `location.city` or `location.country`, its
275
+ resolved value follows the localized profile data returned by the Experience API. The Node SDK is
276
+ stateless, so `getFlag()` does not automatically emit flag-view tracking.
277
+
278
+ ### Caching guidance
279
+
280
+ Cache raw Contentful delivery payloads in your application layer or with the SDK-managed entry
281
+ cache, not profile-evaluated SDK event results. Use `clearContentfulEntryCache()` when an SDK
282
+ instance must drop cached CDA entries.
283
+
284
+ | Data or call | Cache across requests? | Reason |
285
+ | --------------------------------------------------------------------------- | ---------------------- | -------------------------------------------------------------------- |
286
+ | Raw Contentful entries fetched from CDA or the SDK-managed entry cache | Yes | They are content snapshots and can be resolved per request |
287
+ | `resolveOptimizedEntry()` and `getMergeTagValue()` results | Request-local only | Results depend on the current profile and optimization data |
288
+ | `page()`, `identify()`, `screen()`, `track()`, and sticky `trackView()` | No | These calls perform side effects and return request-specific profile |
289
+ | Non-sticky `trackView()`, `trackClick()`, `trackHover()`, `trackFlagView()` | No | These calls emit Insights API events |
290
+
291
+ The
292
+ [Node SDK integration guide](https://contentful.github.io/optimization/documents/Documentation.Guides.integrating-the-node-sdk-in-a-node-app.html)
293
+ covers request context, profile persistence, Contentful entry resolution, and hybrid Node + browser
294
+ setups in detail.
295
+
296
+ ## Development harness
297
+
298
+ The package-local dev harness runs from `packages/node/node-sdk/dev/` and reads `.env` from this
299
+ package directory.
300
+
301
+ 1. Start from `.env.example` and create or update `packages/node/node-sdk/.env`.
302
+ 2. Prefer the repo-standard `PUBLIC_...` variable names shown in `.env.example`.
303
+ 3. Start the harness from the repo root:
304
+
305
+ ```sh
306
+ pnpm --filter @contentful/optimization-node dev
307
+ ```
308
+
309
+ ## Related
310
+
311
+ - [Integrating the Optimization Node SDK in a Node app](https://contentful.github.io/optimization/documents/Documentation.Guides.integrating-the-node-sdk-in-a-node-app.html) -
312
+ step-by-step server-side integration guide
313
+ - [Node SDK generated reference](https://contentful.github.io/optimization/modules/_contentful_optimization-node.html) -
314
+ exported API reference
315
+ - [Node reference implementation](https://github.com/contentful/optimization/blob/optimization-node-v1.0.0/implementations/node-sdk/README.md) - server-rendered
316
+ implementation using request-scoped SDK calls
317
+ - [Node + Web reference implementation](https://github.com/contentful/optimization/blob/optimization-node-v1.0.0/implementations/node-sdk+web-sdk/README.md) -
318
+ hybrid SSR and browser implementation
319
+ - [Optimization Web SDK](https://github.com/contentful/optimization/blob/optimization-node-v1.0.0/packages/web/web-sdk/README.md) - browser SDK used when the same application
320
+ also needs client-side consent, persistence, tracking, or live updates