Use this guide when a plain browser app or custom JavaScript adapter uses
@ninetailed/experience.js and you want to move to @contentful/optimization-web.
The legacy browser client owns plugin arrays, legacy storage keys, browser globals, and imperative
event methods. The Web SDK uses one ContentfulOptimization instance for the browser runtime. Your
app owns Contentful fetching, routing, rendering, consent records, identity policy, and any
third-party forwarding.
Follow the Web SDK integration guide for the target setup details.
Gather these inputs:
Ninetailed, any injected instance, and every plugin passed to it.page, track, identify, batch, reset, debug, flag reads, and manual rendering
helpers.window.ninetailed, __nt_anonymous_id__, __anon_id, __nt_profile__,
__nt_experiences__, __nt_changes__, __nt_debug__, __nt-consent__, or ntaid.clientId, environment, locale, and browser-visible config convention.
These values identify the Contentful project and locale the browser runtime may call; keep secrets
out of them and follow your bundler's public-variable convention.nt_* fields. Start
with one authored all-visitors variant. See
Migrating an experience.js Contentful model to Optimization.@contentful/optimization-web and create one Web SDK instance for the browser runtime.page() call and entry resolution using the Web SDK guide.identify, track, flags, reset, and consent with target SDK calls and app-owned policy.@ninetailed/experience.js and legacy plugin packages after imports are gone.Start with the file that creates new Ninetailed(...) or injects an existing instance into a
provider. List every plugin and every module that reads window.ninetailed. Legacy provider props
did not re-create the client after construction, so treat client setup as a single runtime boundary
rather than a reactive config surface.
Also inventory persistence. Legacy browser storage and the ntaid cookie do not map directly to
the target cookie name or consent model. Decide whether to reset visitor identity during migration
or perform an app-owned continuity handoff.
Create one ContentfulOptimization instance and reuse it. The Web SDK is ready immediately after
construction, but optimization state is empty until an accepted event such as page() or
identify() returns selections. An accepted event returns { accepted: true, data? }; stateful
browser runtimes also expose accepted events on states.eventStream. Keep the order:
Do not preserve legacy plugin arrays. Replace each plugin concern with the target SDK surface or an app-owned integration.
Map imperative calls by ownership:
page() and route tracking move to Web SDK event methods or trackCurrentPage().identify({ userId, traits }) requires a user ID and returns an accepted or blocked result.track({ event, properties }) owns application events.reset() clears SDK profile state and the SDK-owned ctfl-opt-aid cookie, but it does not clear
your consent record.The target consent model has two axes: event consent controls whether SDK events may be sent, and
persistence consent controls whether the SDK may store the profile-id cookie. With the default
pre-consent allow-list, page and identify can be admitted before explicit consent; set
allowedEventTypes: [] if your policy requires every event to wait.
Stop feeding mapped experience configuration into browser rendering. Pass a fetched baseline entry
to resolveOptimizedEntry() or use the Web Components path from the Web guide. When no selection
exists, links are unresolved, or the payload uses all locales, the resolver returns the baseline.
Replace legacy flag hooks with target flag reads. Stateful flag reads auto-attempt flag-view
tracking when consent and profile state allow it; trackFlagView() is the explicit manual path.
Use the Web guide's production checks, then add migration-specific checks:
states.eventStream; consent-blocked diagnostics appear on
states.blockedEventStream.window.ninetailed or legacy storage keys.messageId.@ninetailed/experience.js, window.ninetailed, and legacy storage keys.| Symptom | Check |
|---|---|
| Entry resolution always returns baseline | Confirm an accepted page or identify event returned selections before resolution, and confirm the Contentful payload contains linked Optimization entries. |
| A second SDK instance throws | Keep one Web SDK instance per browser runtime; destroy only during teardown. |
| Vendor analytics stopped receiving events | Replace legacy plugins with event-stream forwarding from the target SDK. |
| Flags read but views are missing | Confirm consent, profile availability, and whether you need explicit trackFlagView(). |