> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://contentful.com/developers/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://contentful.com/developers/docs/_mcp/server.

# Migrate experience.js Next.js to the Pages Router SDK

> Use this guide when a Pages Router app uses `@ninetailed/experience.js-next`, SSR plugin behavior, or legacy React surfaces and you want to move to the Optimization Pages Router SDK.

## Overview

Use this guide when a Pages Router app uses `@ninetailed/experience.js-next`, SSR plugin behavior,
or legacy React surfaces and you want to move to the Optimization Pages Router SDK.

## What changes

The Pages Router target uses `@contentful/optimization-nextjs/pages-router` for browser components
and `@contentful/optimization-nextjs/pages-router/server` for `getServerSideProps`. Server props
own request evaluation and profile continuity; the browser root receives a request handoff and
continues with React Web behavior.

Start with the
[Next.js Pages Router integration guide](/personalization/optimization-sdk/integrate-the-optimization-sdk-in-a-nextjs-pages-router-app/).

## Before you migrate

Gather these inputs:

* Provider and tracker placement in `_app.tsx`.
* Every `getServerSideProps` path that uses SSR plugin behavior or `ntaid`.
* Legacy React components, hooks, flags, and mapper-dependent entries.
* Consent cookie, profile cookie, and initial page-event behavior.
* Any analytics, privacy, preview, or insights plugins.

## Migration path

1. Confirm this app should use Pages Router through
   [Choose a Next.js migration path from experience.js](/personalization/optimization-sdk/choose-a-nextjs-migration-path-from-experience-js/).
2. Migrate authored Contentful entries when legacy mapper output is still required. See
   [Migrate an experience.js Contentful model to Optimization](/personalization/optimization-sdk/migrate-an-experiencejs-contentful-model-to-optimization/).
3. Create the Pages Router client and server bindings.
4. Replace SSR plugin profile and page evaluation in `getServerSideProps`.
5. Replace personalized rendering with the bound Pages Router `OptimizedEntry`.
6. Replace client-side extras through React Web behavior and the plugin migration guide.
7. Remove legacy Next, React, and plugin packages after imports are gone.

## Replace legacy surfaces

### Inventory legacy Pages Router wiring

Record where the legacy provider and tracker mount, whether `onRouteChange` replaces default page
calls, and which pages use SSR plugin helpers. Also record any code that reads or writes `ntaid`,
because target profile continuity uses the SDK-owned `ctfl-opt-aid` cookie.

### Install and bind the Pages Router SDK

Use the target guide to create both bindings:

* Client binding helper from `@contentful/optimization-nextjs/pages-router`.
* Server binding helper from `@contentful/optimization-nextjs/pages-router/server`.

Mount the target `OptimizationRoot` and `NextPagesAutoPageTracker` in `_app.tsx`, passing
`pageProps.contentfulOptimization.handoff` to the root. `contentfulOptimization` is an app-owned page
props wrapper; its `handoff` field is the SDK `BrowserOptimizationHandoff` returned by the server
binding's `createRequestHandoff(context, options)` helper. The handoff can contain browser consent defaults,
request-scoped optimization state, managed entries, and the required `initialPageEvent` value.

The root consumes the handoff's initial-page instruction. Keep the separate tracker mounted with
`initialPageEvent={handoff ? 'skip' : 'emit'}` so it skips the first route whenever the root has a
handoff and emits only when no handoff exists.

If migrated components will use `<OptimizedEntry entryId>`, configure the server binding with the
app's `contentful` client. Pass `prefetchManagedEntries` descriptors—entry IDs or objects containing
`entryId` and optional `entryQuery`—in the `options` passed to
`createRequestHandoff(context, options)`. The helper fetches those baselines and adds them to
`handoff.entries` before the props reach the root. The Pages Router client binding does not fetch
managed entries by itself.

### Replace server profile and page evaluation

In an app-owned server module, call `bindNextjsPagesRouterServerOptimization(config)` once and
destructure its returned `createRequestHandoff` helper. Call
`createRequestHandoff(context, options)` inside `getServerSideProps`, then assign the returned
handoff to the app-owned `contentfulOptimization.handoff` prop. If your app wraps this sequence in a
helper, define that helper in the server module before importing it into a page.

The server binding resolves request consent, emits the first page event when allowed, writes the
anonymous-id cookie when profile persistence permits it, and returns the request handoff. Observe
accepted server evaluation by checking
`contentfulOptimization.handoff.initialPageEvent === 'skip'`; observe denied consent by checking
that no Experience API call is made and the value is `'emit'`.

Pass the handoff to `OptimizationRoot`. The root follows its `initialPageEvent` value, while
`NextPagesAutoPageTracker` uses `initialPageEvent={handoff ? 'skip' : 'emit'}` to avoid duplicating
the root's first-route decision. Keep legacy route-change code removed.

### Replace personalized rendering

Replace legacy React wrappers and mapper output with the Pages Router `OptimizedEntry`. It accepts a
manual `baselineEntry` or an `entryId` path backed by baselines in `handoff.entries`. Create those
entries by passing `prefetchManagedEntries` descriptors in the `options` argument to
`createRequestHandoff(context, options)`. It can use per-entry loading, error, and live-update props
because it is the React Web component bound for Pages Router.

If the first render depends on legacy `nt_*` fields, migrate the Contentful model before replacing
the component.

### Replace client-side extras

Client features use the React Web runtime:

* Flags use target flag reads and optional `trackFlagView()`.
* Analytics vendors use accepted and blocked event streams.
* Consent uses app-owned policy passed to server and browser SDK surfaces.
* Preview attaches to the live browser SDK through the preview panel package.

Use [Migrate experience.js plugins and preview](/personalization/optimization-sdk/migrate-experiencejs-plugins-and-preview/)
for plugin-specific replacement.

### Validate Pages Router migration

Verify the server and browser handoff:

* The server binding's `createRequestHandoff(context, options)` runs in `getServerSideProps` on the
  personalized page.
* The app-owned `pageProps.contentfulOptimization.handoff` reaches `OptimizationRoot` in `_app.tsx`.
* The handoff records accepted server evaluation with `initialPageEvent: 'skip'`, and the separate
  tracker skips whenever that handoff is present.
* A target `OptimizedEntry` renders a variant or baseline.
* Personalized results are not cached outside the request boundary.

## Validate the migration

* Search for `@ninetailed/experience.js-next`, SSR plugin imports, `ntaid`, and legacy React
  surfaces.
* Verify accepted server evaluation and denied-consent behavior.
* Verify all-locale Contentful payloads are not used for optimized entries.
* Verify client-side plugin replacements only after the route and rendering work.

## Troubleshooting

| Symptom                                           | Check                                                                                                                     |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| First page events duplicate                       | Pass the handoff to `OptimizationRoot`; set `NextPagesAutoPageTracker` to `initialPageEvent={handoff ? 'skip' : 'emit'}`. |
| `getServerSideProps` returns a 500 on API failure | Wrap the server helper and render baseline on failure when your app needs graceful fallback.                              |
| Browser render cannot find managed entries        | Pass `prefetchManagedEntries` descriptors in the `options` argument to `createRequestHandoff(context, options)`.          |
| Hooks import fails                                | Import React Web hooks from `@contentful/optimization-nextjs/client`, not `/pages-router`.                                |

## Related guides

* [Next.js Pages Router integration guide](/personalization/optimization-sdk/integrate-the-optimization-sdk-in-a-nextjs-pages-router-app/)
* [Choose a Next.js migration path from experience.js](/personalization/optimization-sdk/choose-a-nextjs-migration-path-from-experience-js/)
* [Migrate an experience.js Contentful model to Optimization](/personalization/optimization-sdk/migrate-an-experiencejs-contentful-model-to-optimization/)
* [Migrate experience.js plugins and preview](/personalization/optimization-sdk/migrate-experiencejs-plugins-and-preview/)
* [Profile synchronization between client and server](/personalization/optimization-sdk/profile-synchronization-between-client-and-server/)
* [Pages Router reference implementation](https://github.com/contentful/optimization/blob/optimization-swift-v2.0.1/implementations/nextjs-sdk_pages-router/README.md)