Migrate experience.js Next.js to the 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.
Before you migrate
Gather these inputs:
- Provider and tracker placement in
_app.tsx. - Every
getServerSidePropspath that uses SSR plugin behavior orntaid. - 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
- Confirm this app should use Pages Router through Choose a Next.js migration path from experience.js.
- Migrate authored Contentful entries when legacy mapper output is still required. See Migrate an experience.js Contentful model to Optimization.
- Create the Pages Router client and server bindings.
- Replace SSR plugin profile and page evaluation in
getServerSideProps. - Replace personalized rendering with the bound Pages Router
OptimizedEntry. - Replace client-side extras through React Web behavior and the plugin migration guide.
- 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 for plugin-specific replacement.
Validate Pages Router migration
Verify the server and browser handoff:
- The server binding’s
createRequestHandoff(context, options)runs ingetServerSidePropson the personalized page. - The app-owned
pageProps.contentfulOptimization.handoffreachesOptimizationRootin_app.tsx. - The handoff records accepted server evaluation with
initialPageEvent: 'skip', and the separate tracker skips whenever that handoff is present. - A target
OptimizedEntryrenders 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.