Was this page helpful?

Set up Experiences SDK with Next.js

Prerequisites

Please read through the installation guide to ensure you meet the prerequisites and have the Experiences SDK installed in your project before starting.

Then, follow the steps below for the type of Next.js project you are using:

Setting up a Next.js project with app router

This guide walks you through on setting up Studio Experiences with your Next.js app using app router.

Studio Experiences does not currently support registering React Server Components as Studio components. Studio components must be client components, marked with the 'use client' directive. However, client components can be initially rendered on the server, allowing you to do server-side rendering.

We are currently investigating adding support for server components.

Rendering an experience

Experiences are rendered with the ExperienceRoot component. Currently, they must be rendered in a client component when using Next.js app router. Let's create a new folder at src/components, in that folder a new component file named Experience.tsx, and add the following:

// src/components/Experience.tsx
'use client';
import { ExperienceRoot } from '@contentful/experiences-sdk-react';
import React from 'react';

interface ExperienceProps {
  experienceJSON: string | null;
  locale: string;
}

const Experience: React.FC<ExperienceProps> = ({ experienceJSON, locale }) => {
  return <ExperienceRoot experience={experienceJSON} locale={locale} />;
};

export default Experience;

The Experience component is a client wrapper (using the 'use client' directive) for the ExperienceRoot component. The experienceJSON and locale props are passed through to the component.

Next, we render the Experience component from a page in our Next.js app. Create a file at src/app/[slug]/pages.tsx and add the following code:

import { createClient } from 'contentful';
import {
  detachExperienceStyles,
  fetchBySlug,
} from '@contentful/experiences-sdk-react';
import Experience from '@/components/Experience';

const accessToken = 'your_access_token';
const space = 'your_space_id';
const environment = 'your_contentful_environment';
const experienceTypeId = 'id_of_experience_content_type';

const client = createClient({
  space,
  environment,
  accessToken,
});

type Props = {
  params: Promise<{ slug: string }>;
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};

async function AppPage({ params, searchParams }: Props) {
  const { slug } = await params;
  const locale = 'en-US';
  const { expEditorMode } = await searchParams;

  const experience = await fetchBySlug({
    client,
    slug,
    experienceTypeId,
    localeCode: locale,
    isEditorMode: expEditorMode === 'true',
  });

  // extract the styles from the experience
  const stylesheet = experience ? detachExperienceStyles(experience) : null;

  // experience currently needs to be stringified manually to be passed to the component
  const experienceJSON = experience ? JSON.stringify(experience) : null;

  return (
    <main style={{ width: '100%' }}>
      {stylesheet && <style>{stylesheet}</style>}
      <Experience experienceJSON={experienceJSON} locale={locale} />
    </main>
  );
}

export default AppPage;

There is a lot happening in this file so let's break it down.

First, we set some values needed to create a Contentful client object in order to make requests to the Contentful API. We suggest not storing these values in your source code but using environment variables to pull them in at build time.

  • accessToken and space - You can find the token and space values in your Contentful space API keys settings, which you can get to either by pressing Cmd/Ctrl + 'k' and typing 'API keys' or by navigating to the settings in the Contentful app.
  • experienceTypeId - This is the id of the content model set up for experiences. Open the content model in the Contentful app and click the "Copy ID" button to get the id.
  • environment - This is the environment you are using in Contentful. You can find this in the Contentful app by clicking on the settings and going to environments.

When fetching an experience, you pass in a slug (that uniquely identifies the experience) and a locale (to fetch the experience in the correct language). We collect the slug from the URL params, and hard-code the locale to 'en-US', though we could also pass it in dynamically.

The search params might contain a query param named expEditorMode to signify that the page is being loaded in Studio's editor. We pass this value to the fetchBySlug method to help determine if the experience should be fetched in editor mode. While in editor mode, the experience is not fetched at this time, but is instead passed into the ExperienceRoot component dynamically during editing.

With all the needed information to make a fetch request, we can now do so by calling the fetchBySlug method.

Once we have the experience, we extract the CSS values from the experience by calling the detachExperienceStyles method. This will give us a stylesheet representing all the design value options that are configured. We write out this stylesheet to a "style" tag in the rendered JSX.

Last, we serialize the experience object to JSON, because the experience object is not serializable directly by Next.js due to their strict JSON serialization techniques. Once that is done, we render the Experience component we created earlier and pass in the experienceJSON and locale params as props.

Everything is now set up in your app to render an experience. Launch your app in your local development server, log into the Contentful portal, sure a content preview URL is configured to point to your local web development server and create a new experience.

To see more detailed info on how to use Experiences with Next.js App Router, check out the Next.js App Router example.

Using custom components

We provide several basic components out of the box, but you can also use your own custom components. To demonstrate how to use custom components with Experiences and Next.js, we will create a simple button component that is configurable from Studio.

Create a new file at src/components/Button.tsx and add the following:

// src/components/Button.tsx
'use client';
import React from 'react';

interface ButtonComponentProps {
  text: string;
}

export const Button: React.FC<ButtonComponentProps> = ({ text, ...props }) => {
  return <button {...props}>{text}</button>;
};

Note that the Button component is marked with the 'use client' directive, which tells React that this component is a client-side component. This is required for all components that are used in Studio Experiences.

Next, register your component with the Experiences SDK. This is done with the help of the defineComponents function which is imported from the @contentful/experiences-sdk-react package. We suggest creating a specific file for handling all your component registration and setup. Create a file at src/studio-config.ts and add the following:

// src/studio-config.ts
import { defineComponents } from '@contentful/experiences-sdk-react';
import { Button } from './components/Button';

defineComponents([
  {
    component: Button,
    definition: {
      id: 'custom-button',
      name: 'Button',
      category: 'Custom Components',
      variables: {
        text: {
          displayName: 'Text',
          type: 'Text',
          defaultValue: 'Click me',
        },
      },
    },
  },
]);

Above, we import the Button component and register it with Experiences by providing a component definition to defineComponents. This definition includes:

  • id - used to identify the component
  • name - the name of the component as it appears in Experiences
  • category - the category the component will be listed under in Experiences
  • variables - the variables that can be configured in Experiences

The most interesting part of the configuration here are the variables, in which we configure one named 'text'. This variable is of type 'Text' and has a default value of 'Click me'. This variable is configurable in Experiences and will be passed to the Button component as a prop.

To learn more about how you can configure your components, check out the Register custom components guide.

Since our Next.js app is running components on both the server-side and client-side, we need to make sure this config file is imported in both places.

To import the studio-config.ts file in server-side, we suggest adding it to a server component that is rendered from the app directory. We can use the page we set up earlier, src/app/[slug]/pages.tsx. Add the following import at the top of the file:

// src/app/[slug]/pages.tsx
//import the studio config server-side
import '@/studio-config';

For client-side, we suggest importing the studio-config.ts file in the src/components/Experience.tsx file. Add the following import at the top of the file:

// src/components/Experience.tsx
//import the studio config client-side
import '@/studio-config';

With the Button component registered, you can now use it in Studio. Open an experience and add the Button component by dragging your button from the 'Custom Components' section. You should see the 'Text' variable in the component content panel on the right, where you can change the text that appears on the button.

Setting up a Next.js project with pages router

This guide walks you through on setting up Studio Experiences with Next.js app using pages router.

Rendering an experience

Experiences are rendered with the ExperienceRoot component, which we will set up in a Next.js page. Create a new file at src/pages/[slug].tsx and add the following:

// src/pages/[slug].tsx
import { createClient } from 'contentful';
import {
  ExperienceRoot,
  detachExperienceStyles,
  fetchBySlug,
} from '@contentful/experiences-sdk-react';
import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';
import Head from 'next/head';

const accessToken = 'your_access_token';
const space = 'your_space_id';
const environment = 'your_contentful_environment';
const experienceTypeId = 'id_of_experience_content_type';
const localeCode = 'en-US';

const client = createClient({
  space,
  environment,
  accessToken,
});

function MyPage({
  experienceJSON,
  stylesheet,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  return (
    <>
      {stylesheet && (
        <Head>
          <style data-ssg>{stylesheet}</style>
        </Head>
      )}
      <main style={{ width: '100%' }}>
        <ExperienceRoot experience={experienceJSON} locale={localeCode} />
      </main>
    </>
  );
}

export const getServerSideProps = async ({
  params,
  query,
}: GetServerSidePropsContext<{ slug: string }, { expEditorMode?: string }>) => {
  const { slug = 'home-page' } = params || {};
  const { expEditorMode } = query;
  const locale = 'en-US';
  const experience = await fetchBySlug({
    client,
    slug,
    experienceTypeId,
    localeCode: locale,
    isEditorMode: expEditorMode === 'true',
  });

  // extract the styles from the experience
  const stylesheet = experience ? detachExperienceStyles(experience) : null;

  // experience currently needs to be stringified manually to be passed to the component
  const experienceJSON = experience ? JSON.stringify(experience) : null;

  return {
    props: {
      experienceJSON,
      stylesheet,
    },
  };
};

export default MyPage;

There is a lot happening in this file so let's break it down.

First, we set some values needed to create a Contentful client object in order to make requests to the Contentful API. We suggest not storing these values in your source code but using environment variables to pull them in at build time.

The variables can be found in the Contentful app in the following places:

  • accessToken and space - You can find the token and space values in your Contentful space API keys settings, which you can get to either by pressing Cmd/Ctrl + 'k' and typing 'API keys' or by navigating to the settings in the Contentful app.
  • experienceTypeId - This is the id of the content model set up for experiences. Open the content model in the Contentful app and click the "Copy ID" button to get the id.
  • environment - This is the environment you are using in Contentful. You can find this in the Contentful app by clicking on the settings and going to environments.

The MyPage component receives the props experienceJSON, locale, and stylesheet from the getServerSideProps method. The stylesheet is written out to a "style" tag in the rendered JSX, and the experienceJSON and locale are passed to the ExperienceRoot component.

The getServerSideProps method is used to fetch the experience and stylesheet server-side and pass it to the MyPage component as props.

When fetching an experience, you pass in a slug (that uniquely identifies the experience) and a locale (to fetch the experience in the correct language). We collect the slug from the URL params, and hard-code the locale to 'en-US', though we could also pass it in dynamically.

The search params might contain a query param named expEditorMode to signify that the page is being loaded in Studio's editor. We pass this value to the fetchBySlug method to help determine if the experience should be fetched in editor mode. While in editor mode, the experience is not fetched at this time, but is instead passed into the ExperienceRoot component dynamically during editing.

With all the needed information to make a fetch request, we can now do so by calling the fetchBySlug method.

Once we have the experience, we extract the CSS values from the experience by calling the detachExperienceStyles method. This will give us a stylesheet representing all the design value options that are configured. We write out this stylesheet to a "style" tag in the rendered JSX.

Last, we serialize the experience object to JSON, because the experience object is not serializable directly by Next.js due to their strict JSON serialization techniques. Once that is done, we return all the data as props to the MyPage component.

Everything is now set up in your app to render an experience. Launch your app in your local development server, log into the Contentful portal, ensure a content preview URL is configured to point to your local web development server and create a new experience.

Using custom components

We provide several basic components out of the box, but you can also use your own custom components. To demonstrate how to use custom components with Experiences and Next.js, we will create a simple button component that is configurable from Studio.

Create a new file at src/components/Button.tsx and add the following:

// src/components/Button.tsx
import React from 'react';

interface ButtonComponentProps {
  text: string;
}

export const Button: React.FC<ButtonComponentProps> = ({ text, ...props }) => {
  return <button {...props}>{text}</button>;
};

Next, your component needs to be registered with the Experiences SDK. This is done with the help of the defineComponents function which is imported from the @contentful/experiences-sdk-react package. We suggest creating a specific file for handling all your component registration and setup. Create a file at src/studio-config.ts and add the following:

// src/studio-config.ts
import { defineComponents } from '@contentful/experiences-sdk-react';
import { Button } from './components/Button';

defineComponents([
  {
    component: Button,
    definition: {
      id: 'custom-button',
      name: 'Button',
      category: 'Custom Components',
      variables: {
        text: {
          displayName: 'Text',
          type: 'Text',
          defaultValue: 'Click me',
        },
      },
    },
  },
]);

Above, we import the Button component and register it with Studio by providing a component definition to defineComponents. This definition includes:

  • id - used to identify the component
  • name - the name of the component as it appears in Studio
  • category - the category the component will be listed under in Studio
  • variables - the variables that can be configured in Studio

The most interesting part of the config here is the variables, in which we configure one named 'text'. This variable is of type 'Text' and has a default value of 'Click me'. This variable will be configurable in Studio and will be passed to the Button component as a prop.

There is a lot more you can configure for your components, but for now, we will keep it simple. For more information, check out the Register Custom Components guide.

To import the studio-config.ts file, we suggest adding it to a component that is rendered from the pages directory. We can use the page we set up earlier, src/pages/[slug].tsx. Add the following import at the top of the file:

// src/pages/[slug].tsx
import '@/studio-config';

With the Button component registered, you can now use it in Studio. Open an experience and add the Button component by dragging your button from the 'Custom Components' section. You should see the 'Text' variable in the component content panel on the right, where you can change the text that appears on the button.