1. Upgrade Content SDK 1.2.0 Next.js apps to version 1.3.1

Migrate your Content SDK 1.3.1 Pages Router app to App Router

Version:

This topic takes you through the steps required to migrate your Pages Router application to App Router. The topic assumes that you're using Content SDK Pages Router application on version 1.3.1. Content SDK 1.3.1 includes the general release of Next.js App Router and several other improvements and bug fixes. Note that version 1.3.0 was released and deprecated due to a critical vulnerability related to React Server Components.

Before you begin
  • Ensure that you're using Pages Router on Content SDK 1.3.1 before following this guide. If you haven't already upgraded to Content SDK 1.3.1, follow the upgrade guide.

  • Review the changelogs for 1.3.0 and 1.3.1. If your application is heavily customized, the changelog can provide guidance on what additional changes you need that are not covered in this topic.

This topic covers the following subjects:

Create a template App Router application

To simplify the migration process as much as possible, create a Content SDK Next.js App Router application. You can then copy some files from this template app into your existing app.

To scaffold the new App Router template app, run the following command:

npx create-content-sdk-app@latest "nextjs-app-router"

Update the Next.js template files in your existing app

The following sections explain how to synchronize files in your existing Content SDK application with corresponding files from the template App Router app.

Update core Content SDK files

This sections updates your existing app's core files to match the new Content SDK app.

To update your core Content SDK files:

  1. Start the migration by moving your logic into the new file structure. Copy the src/app folder from the sample App Router app into the app you are upgrading. Check the table below and take note of any custom logic in these files and the places this custom logic should move to:

    Existing file

    New file

    pages/_app.tsx

    app/layout.tsx / app/[site]/layout.tsx

    If _app.tsx has been importing CSS rules previously, move the import into app/globals.css

    If you’ve been using Tailwind CSS, consider enabling it via PostCSS in the App Router app.

    The Skate Park starter sample in the xmcloud-starter-js repository contains an example on how to do it.

    pages/[[…path]].tsx

    app/[site]/[…path]/page.tsx

    pages/500.tsx

    app/global-error.tsx

    pages/404.tsx

    app/[site]/[…path]/not-found.tsx

    pages/api/%path%/%api-route%.tsx

    app/api/%path%/%api-route%/route.ts

    If there’s no custom logic, remove the old files and keep the files from the newly scaffolded app into your existing app.

  2. If you haven't customized src/Bootstrap.tsx, replace it with the one from the sample App Router app. Otherwise, make the following changes:

    • Add the following at the start of the file:

      'use client';
    • Remove the following import statement:

      import { SitecorePageProps } from '@sitecore-content-sdk/nextjs';
    • Find the following:

      const Bootstrap = (props: SitecorePageProps): JSX.Element | null => {
        const { page } = props;
    • Replace it with the following:

      const Bootstrap = ({
        siteName,
        isPreviewMode,
      }: {
        siteName: string;
        isPreviewMode: boolean;
      }): JSX.Element | null => {
    • Find the following useEffect portion:

      useEffect(() => {
        if (!page) {
          return;
        }
      
        const mode = page.mode;
        if (process.env.NODE_ENV === 'development') {
          console.debug('Browser Events SDK is not initialized in development environment');
        } else if (!mode.isNormal) {
          console.debug('Browser Events SDK is not initialized in edit and preview modes');
        } else {
          if (config.api.edge?.clientContextId) {
            CloudSDK({
              sitecoreEdgeUrl: config.api.edge.edgeUrl,
              sitecoreEdgeContextId: config.api.edge.clientContextId,
              siteName: page.siteName || config.defaultSite,
              enableBrowserCookie: true,
              // Replace with the top level cookie domain of the website that is being integrated e.g ".example.com" and not "www.example.com"
              cookieDomain: window.location.hostname.replace(/^www\./, ''),
            })
              .addEvents()
              .initialize();
          } else {
            console.error('Client Edge API settings missing from configuration');
          }
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, [page?.siteName]);
    • Replace it with the following:

      useEffect(() => {
        if (process.env.NODE_ENV === "development") {
          console.debug(
            "Browser Events SDK is not initialized in development environment"
          );
          return;
        }
      
        if (isPreviewMode) {
          console.debug(
            "Browser Events SDK is not initialized in edit and preview modes"
          );
          return;
        }
      
        if (config.api.edge?.clientContextId) {
          console.log("✨ Initializing CloudSDK for site:", siteName);
          CloudSDK({
            sitecoreEdgeUrl: config.api.edge.edgeUrl,
            sitecoreEdgeContextId: config.api.edge.clientContextId,
            siteName: siteName || config.defaultSite,
            enableBrowserCookie: true,
            cookieDomain: window.location.hostname.replace(/^www\./, ""),
          })
            .addEvents()
            .initialize();
        } else {
          console.error("Client Edge API settings missing from configuration");
        }
      }, [siteName, isPreviewMode]);
  3. If you haven't customized src/Layout.tsx, replace it with the one from sample App Router app. Otherwise, make the following changes:

    • Find the following imports:

      import Head from 'next/head';
      import { Placeholder, Field, DesignLibrary, Page } from '@sitecore-content-sdk/nextjs';
      import SitecoreStyles from 'src/components/content-sdk/SitecoreStyles';
    • Replace them with the following imports:

      import { Field, Page } from '@sitecore-content-sdk/nextjs';
      import SitecoreStyles from 'components/content-sdk/SitecoreStyles';
      import { DesignLibraryApp } from "@sitecore-content-sdk/nextjs";
      import AppPlaceholder from 'components/content-sdk/Placeholder';
      import componentMap from '.sitecore/component-map';
    • Find the RouteFields interface and make it exportable as shown:

      export interface RouteFields {
        [key: string]: unknown;
        Title?: Field;
      }
    • Remove the following lines from the Layout function implementation, if present:

      const fields = route?.fields as RouteFields;
      ...
      const importMapDynamic = () => import('.sitecore/import-map');
      ...
      <Head>
          <title>{fields?.Title?.value?.toString() || 'Page'}</title>
          <link rel="icon" href="/favicon.ico" />
      </Head>
    • Find the DesignLibrary rendering:

      <DesignLibrary loadImportMap={importMapDynamic} />
    • Replace it with the following:

      <DesignLibraryApp
          page={page}
          rendering={route}
          componentMap={componentMap}
          loadServerImportMap={() => import(".sitecore/import-map.server")}
      />
    • Replace all the Placeholder usages with AppPlaceholder. For example, for headless-header placeholder, find the following:

      {route && <Placeholder name="headless-header" rendering={route} />}
    • Replace it with the following:

      {route && (
          <AppPlaceholder
            page={page}
            componentMap={componentMap}
            name="headless-header"
            rendering={route}
          />
      )}
  4. If you haven't customized src/Scripts.tsx, replace it with the one from the sample App Router app. Otherwise, make the following changes:

    • Add the following at the start of the file:

      'use client';
    • Remove the following import:

      import FEAASScripts from 'components/content-sdk/FEAASScripts';
    • Remove the following line:

      <FEAASScripts />
  5. Remove src/NotFound.tsx if present. Move any custom logic to app/not-found.tsx.

  6. Copy src/Providers.tsx from the sample App Router app into your existing application.

  7. In the app's tsconfig.json file:

    • Set target in compilerOptions to ES2017.

    • In the plugins section, add the next plugin as shown:

      "plugins": [
        {
          "name": "next"
        }
      ],
    • In the paths section, add the .sitecore path as shown:

      "paths": {
      ...
        ".sitecore/*": [
          ".sitecore/*"
        ],
      ...

Migrate next.config.js to TypeScript

Rename the next.config.js file to next.config.ts and ensure the following is present in the file in addition to any customizations you may have done:

import type { NextConfig } from 'next';
import createNextIntlPlugin from 'next-intl/plugin';
const nextConfig: NextConfig = {
  // Allow specifying a distinct distDir when concurrently running app in a container
  distDir: process.env.NEXTJS_DIST_DIR || '.next',
  // Enable React Strict Mode
  reactStrictMode: true,
  // Disable the X-Powered-By header. Follows security best practices.
  poweredByHeader: false,
  // use this configuration to ensure that only images from the whitelisted domains
  // can be served from the Next.js Image Optimization API
  // see https://nextjs.org/docs/app/api-reference/components/image#remotepatterns
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'edge*.**',
        port: '',
      },
      {
        protocol: 'https',
        hostname: 'xmc-*.**',
        port: '',
      },
    ],
  },
  // use this configuration to serve the sitemap.xml and robots.txt files from the API route handlers
  rewrites: async () => {
    return [
      {
        source: '/sitemap:id([\\w-]{0,}).xml',
        destination: '/api/sitemap',
        locale: false,
      },
      {
        source: '/robots.txt',
        destination: '/api/robots',
        locale: false,
      },
    ];
  },
};
const withNextIntl = createNextIntlPlugin();
export default withNextIntl(nextConfig); 

Update localization

Pages Router Content SDK uses the I18nProvider imported from the 'next-localization' library out of the box to enable localization. App Router uses the next-intl package instead.

To migrate to the new implementation:

  1. In your existing app's package.json file, replace the "next-localization": "^0.12.0", dependency with "next-intl": "^4.3.5",.

  2. Copy the src/i18n folder from the sample App Router app into your existing app.

  3. Use NextIntlClientProvider instead of I18nProvider by making the following changes:

    • Find the following import:

      import { I18nProvider } from 'next-localization';
    • Replace it with the following:

      import { NextIntlClientProvider } from 'next-intl';
    • Use NextIntlClientProvider as a wrapper context provider for your pages and components.

  4. Use routing imported from 'src/i18n/routing' to access routing informatin for your pages.

Note

Check src\app\[site]\[locale]\[[...path]]\page.tsx in the sample App Router app to see an example of the new localization implementation.

Update SitecoreProvider

Built-in Sitecore contexts were previously applied via two providers (ComponentPropsContext and SitecoreProvider) as shown:

<ComponentPropsContext value={componentProps || {}}>
  <SitecoreProvider componentMap={components} api={scConfig.api} page={page}>
    <Layout page={page} />
  </SitecoreProvider>
</ComponentPropsContext>

Content SDK 1.3.1 streamlines this with the introduction of the src/Providers.tsx file. You can apply required contexts by importing Providers as shown:

import Providers from 'src/Providers';

To use them where both providers where applied before:

<Providers componentProps={componentProps} page={page}>
  <Layout page={page} />
</Providers>
Note

If you need to apply individual providers, you can still do so.

Update BYOC functionality

If you're using BYOC, make the following changes:

  1. In the src/byoc/index.client.tsx file, and the following at the top of the file:

    'use client';
  2. In the src/byoc/index.ts file:

    • Find the following lines:

      const ClientBundle = dynamic(() => import('./index.client'), {
        ssr: false,
      });
    • Replace it with the following:

      const ClientBundle = dynamic(() => import('./index.client'));

Update placeholders

You must replace Placeholder components throughout your app with AppPlaceholder.

To replace it for the headless-header placeholder in Layout.tsx:

  1. Find the following:

    {route && <Placeholder name="headless-header" rendering={route} />}
  2. Replace it with the following:

    {route && (
        <AppPlaceholder
          page={page}
          componentMap={componentMap}
          name="headless-header"
          rendering={route}
        />
    )}
    Important

    Ensure that you import the component map from '.sitecore/component-map'; or your custom component-map location. You must also get the page prop either from the props passed into the server component or from the Sitecore Context via useSitecore() call in client components (const { page } = useSitecore();).

Migrate API routes

If you don’t have any custom routes or modifications to the out-of-the-box Content SDK API routes located under pages/api, copy the /src/app/api folder and all the files under it from the sample App Router application into your existing application.

For any custom routes or customizations to the out-of-the-box routes, migrate them to the new route structure introduced by App Router.

Update components

App Router provides the ability to fine tune your application by converting your components into server or client ones. To mark the built-in components as client components:

  1. For all components under src/components/content-sdk, add the following at the start of the file:

    'use client';
  2. You should mark client components appropriately in component-map.ts or add them to component-map.client.ts. This is only applicable if you're managing your own component-maps.

    To mark your client components in the component-map, use the following example:

    ['Title', {...Title, componentType: 'client'}],
  3. Contexts, such as Sitecore Context, applied via SitecoreProvider and other React providers, will be unavailable in server components. Ensure you pass required data as props into server components.

  4. You can only pass serializable props from server components into client components.

  5. The getServerSideProps function can still work with App Router, but we strongly recommend you utilize Server Functions or server logic instead.

If you have suggestions for improving this article, let us know!