npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@contentful/optimization-node

v1.2.1

Published

Node.js SDK for Contentful Optimization

Readme

Guides · Reference · Contributing

The Optimization Node SDK implements stateless server-side optimization behavior on top of the Optimization Core SDK. Use it for server rendering, server functions, and Node services that need request-scoped profile evaluation or event emission.

If you are integrating a Node application, start with Getting Started, then use Integrating the Optimization Node SDK in a Node app for the step-by-step flow. This README keeps the package orientation and common setup options close at hand; generated reference documentation remains the source of truth for exported API signatures.

Getting started

Install using an NPM-compatible package manager, pnpm for example:

pnpm add @contentful/optimization-node

Add contentful too when the SDK will use your app-owned contentful.js client for managed entry fetching.

Import the Optimization class; both CJS and ESM module systems are supported, ESM preferred:

import ContentfulOptimization from '@contentful/optimization-node'

Create the SDK once per module or process, then bind consent and request context in each route:

const optimization = new ContentfulOptimization({
  clientId: 'your-client-id',
  environment: 'main',
  locale: 'en-US',
})

function appPolicyAllowsOptimizationEvent(req: { cookies?: Record<string, string> }): boolean {
  return req.cookies?.['app-personalization-consent'] === 'granted'
}

async function renderRequest(
  req: { cookies?: Record<string, string>; headers: { 'accept-language'?: string } },
  profileId: string,
) {
  const appLocale = getAppLocale(req)
  const requestOptimization = optimization.forRequest({
    consent: {
      events: appPolicyAllowsOptimizationEvent(req),
      persistence: appPolicyAllowsOptimizationEvent(req),
    },
    locale: appLocale,
    eventContext: { locale: appLocale },
    profile: { id: profileId },
  })

  const { accepted, data } = await requestOptimization.page()

  return accepted ? data : undefined
}

When to use this package

Use @contentful/optimization-node for server-side rendering, server functions, and Node services that need stateless profile evaluation or event emission. Use the Web or React Web SDK alongside it when browser-side consent, anonymous ID persistence, automatic interaction tracking, or live updates are part of the same application.

Common configuration

The Node SDK is stateless. It does not maintain consent, profile, or browser persistence state between requests. For cross-SDK consent guidance, see Consent management in the Optimization SDK Suite.

| Option | Required? | Default | Description | | ------------------- | --------- | ---------------------- | ----------------------------------------------------------- | | clientId | Yes | N/A | Shared API key for Experience API and Insights API requests | | environment | No | 'main' | Contentful environment identifier | | api | No | See API options below | Experience API and Insights API endpoint options | | app | No | undefined | Application metadata attached to outgoing event context | | contentful | No | undefined | App-owned contentful.js client, default query, and cache | | locale | No | undefined | Default SDK Experience API and event locale | | fetchOptions | No | SDK defaults | Fetch timeout and retry behavior | | allowedEventTypes | No | ['identify', 'page'] | Event types allowed before request event consent is granted | | eventBuilder | No | Node SDK defaults | Event metadata overrides for SDK-layer authors | | logLevel | No | 'error' | Minimum log level for the default console sink |

Common api options:

| Option | Required? | Default | Description | | ------------------- | --------- | ------------------------------------------ | ------------------------------------------------ | | experienceBaseUrl | No | 'https://experience.ninetailed.co/' | Base URL for the Experience API | | insightsBaseUrl | No | 'https://ingest.insights.ninetailed.co/' | Base URL for the Insights API | | enabledFeatures | No | ['ip-enrichment', 'location'] | Experience API features to apply to each request |

Request-scoped Experience options belong in experienceOptions when creating the request-bound client:

| Option | Description | | ----------- | ------------------------------------------------------------- | | ip | IP address override used by the Experience API | | locale | Locale query parameter for localized Experience API responses | | plainText | Sends performance-critical Experience API endpoints as text | | preflight | Aggregates a new profile state without storing it |

Request-scoped Insights options belong in insightsOptions:

| Option | Description | | -------- | ----------------------------------------------------------------- | | beacon | Last-chance sender for serialized Insights API batches if needed. |

Use the request-scoped top-level locale on forRequest() as the promoted path for localized Experience API responses and default event context. experienceOptions.locale remains available as an advanced low-level pass-through when locale is not supplied. If both are provided, request locale wins.

Common fetchOptions are fetchMethod, requestTimeout, retries, intervalTimeout, onFailedAttempt, and onRequestTimeout. Default retries intentionally apply only to HTTP 503 responses.

Choose the application Contentful locale in your router, i18n layer, or request policy. Pass that value directly to Contentful CDA requests, and pass the same value to forRequest({ locale: appLocale }) when Experience API responses and event context should use the same language. Merge tags that reference localized profile fields such as location.city and location.country then resolve in a language consistent with the rendered content. See Locale handling in the Optimization SDK Suite for the full locale model.

For every option, callback payload, and exported type, use the generated Node SDK reference.

Core workflows

Request-scoped events

Build event context from the incoming request, bind application-owned consent state with forRequest(), then call page(), identify(), screen(), track(), or sticky trackView() on the returned request object:

import type { Request } from 'express'

app.get('/products/:slug', async (req, res) => {
  const appLocale = getAppLocale(req)
  const requestOptimization = optimization.forRequest({
    consent: {
      events: appPolicyAllowsOptimizationEvent(req),
      persistence: appPolicyAllowsOptimizationEvent(req),
    },
    locale: appLocale,
    eventContext: {
      locale: appLocale,
    },
    profile: { id: req.cookies.profileId },
  })
  const { accepted, data: optimizationData } = await requestOptimization.page({
    properties: { path: req.path },
  })

  if (accepted && requestOptimization.canPersistProfile && optimizationData?.profile.id) {
    persistProfileId(res, optimizationData.profile.id)
  }

  res.render('product', { optimizationData })
})

For default-on application policies that do not render an end-user consent UI, replace the request-specific consent lookup with accepted consent:

const requestOptimization = optimization.forRequest({
  consent: { events: true, persistence: true },
  locale: appLocale,
  eventContext: { locale: appLocale },
  profile,
})

Node SDK event calls fail closed except for the default pre-consent allowlist, identify and page. Those allowlisted events are sent with context.gdpr.isConsentGiven: false when request event consent is not granted. Pass allowedEventTypes: [] to require strict opt-in for all stateless event methods.

In stateless runtimes, Insights-backed methods require a request-bound profile for delivery. Non-sticky trackView, trackClick, trackHover, and trackFlagView require a profile ID passed to forRequest().

Content resolution

When a contentful.js client is available, prefer SDK-managed fetching by entry ID or content type and slug. Configure the client once, then call fetchOptimizedEntry() on a request-bound client after an accepted Experience API call. forRequest() clients use the latest selected optimizations when omitted; singleton calls require explicit selectedOptimizations.

const optimization = new ContentfulOptimization({
  clientId: 'client-id',
  contentful: { client: contentfulClient },
  environment: 'main',
  locale: appLocale,
})

const requestOptimization = optimization.forRequest({ consent: true, profile })
await requestOptimization.page()
const { baselineEntry, entry } = await requestOptimization.fetchOptimizedEntry({
  contentType: 'productPage',
  slug: req.params.slug,
  entryQuery: { locale: appLocale },
})

Use fetchContentfulEntries() or prefetchManagedEntries() when a route knows several managed entry sources. A slug source accepts slugField when the content model does not use the default slug field. Slug lookup enforces its content-type, field selector, and two-result uniqueness limit after merging entryQuery; successful resolution uses the fetched entry's sys.id. Slug handoffs nest the normalized descriptor under managedEntry and retain that ID in entryId. Equivalent ID sources retain same-query batching and 100-ID chunking.

If your application already fetched the baseline entry, keep using the manual resolver:

const pageResult = await requestOptimization.page()
const resolvedEntry = optimization.resolveOptimizedEntry(
  baselineEntry,
  pageResult.accepted ? pageResult.data.selectedOptimizations : undefined,
)

Use one CDA locale in either path. For localized apps, configure your application locale. A request-bound client uses forRequest({ locale: appLocale }) as the managed Contentful query locale when neither contentful.defaultQuery nor the per-call query sets locale. Pass the same value to app-owned Contentful CDA requests and to forRequest({ locale: appLocale }) when MergeTags that read localized profile fields match the rendered entry language. Do not pass all-locale CDA responses from withAllLocales or locale=*; the resolver expects direct single-locale field values. See Entry personalization and variant resolution for the entry contract and Locale handling in the Optimization SDK Suite for request locale behavior.

Use getMergeTagValue() for Contentful Rich Text merge tags and getFlag() for Custom Flags. If a merge tag references localized profile fields such as location.city or location.country, its resolved value follows the localized profile data returned by the Experience API. The Node SDK is stateless, so getFlag() does not automatically emit flag-view tracking.

Caching guidance

Cache raw Contentful delivery payloads in your application layer or with the SDK-managed entry cache, not profile-evaluated SDK event results. Use clearContentfulEntryCache() when an SDK instance must drop cached CDA entries.

| Data or call | Cache across requests? | Reason | | --------------------------------------------------------------------------- | ---------------------- | -------------------------------------------------------------------- | | Raw Contentful entries fetched from CDA or the SDK-managed entry cache | Yes | They are content snapshots and can be resolved per request | | resolveOptimizedEntry() and getMergeTagValue() results | Request-local only | Results depend on the current profile and optimization data | | page(), identify(), screen(), track(), and sticky trackView() | No | These calls perform side effects and return request-specific profile | | Non-sticky trackView(), trackClick(), trackHover(), trackFlagView() | No | These calls emit Insights API events |

The Node SDK integration guide covers request context, profile persistence, Contentful entry resolution, and hybrid Node + browser setups in detail.

Development harness

The package-local dev harness runs from packages/node/node-sdk/dev/ and reads .env from this package directory.

  1. Start from .env.example and create or update packages/node/node-sdk/.env.

  2. Prefer the repo-standard PUBLIC_... variable names shown in .env.example.

  3. Start the harness from the repo root:

    pnpm --filter @contentful/optimization-node dev

Related