Was this page helpful?

Set up Experience Builder

Table of contents

What is Experience Builder?

Experience Builder is currently in private alpha testing.

Experience Builder allows you to use your existing components to assemble web experiences without coding. With the Experience Builder, you can also make adjustments to existing experiences without involving your developers.

Setting up an Experience Builder involves the following main stages:

  1. Setup in Contentful.
  2. Project setup.

Setup in Contentful

Setup in Contentful is done by a space admin and requires changes to one of the environments in the Contentful web app.

Add experience type

  1. Log in to the Contentful web app.
  2. Select a space and environment in which you would like to set up Experience Builder.
There must be a minimum of one existing content type in your environment.
  1. Go to the Content model tab. Click Add new and select Experience Type from the drop-down. The “Add a new Experience Type” window is displayed.

Experience builder add experience type

  1. In the Name field, enter a custom name for your experience type. An API identifier is generated based on the name.
  2. Optional: In the Description field, enter a custom description for your duplicated content type.
  3. Click Next. A “Setup your preview screen” is displayed.

Experience builder experience type name

  1. Either select an existing preview URL or enter a new one. This step is required for you to be able to preview your experience while working on it in the Experience Builder.
To try out Experience Builder locally, it might be the best to use a localhost URL. For more information on setting up a content preview, refer to content preview setup guide.
  1. Click Create Experience Type. Your experience type is created and saved.

Experience builder add preview url

The experience type is created and automatically configured with its default fields. Some of them are intentionally disabled for editing or deleting, to ensure a working integration. "Title" and "Slug" fields are optional and can be edited. Localization is optional for both fields.

Project setup

Prerequisites

  • Our SDK has been written in TypeScript (JavaScript) for React. We don’t support any other languages or frameworks at the moment.
  • The SDK works only with React applications which support React hooks (React v16.8+)
  • This guide provides basic setup steps for our SDK. Depending on your setup, you may need to do some extra steps to make it fit properly.
  • Your web application must support being rendered in the iframe. If you have a CSP policy blocking this, you must include https://app.contentful.com into the list of allowed domains.

Setting up the Experience Builder project involves the following main stages:

  1. Install the SDK — Install the Experience Builder SDK and import it into your project.
  2. Configure the SDK — Import components from the React component library of your choice.
  3. Register custom components — Use the function provided by the SDK to register each component that you would like to have in the Experience Builder.

Install the SDK

yarn add @contentful/experience-builder or npm install @contentful/experience-builder -S

Configure the SDK

Import the ExperienceRoot component, provided by the SDK and place it somewhere within the page that you plan to build.

// src/pages/[locale]/[slug].tsx

import React from 'react';
import { createClient } from 'contentful'
import { createExperience, fetchers, ExperienceRoot } from '@contentful/experience-builder';
import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';

const client = createClient({
  space: process.env.CTFL_SPACE_ID,
  environment: process.env.CTFL_ENV_ID,
  host: process.env.CTFL_API_HOST, // Supported values: 'preview.contentful.com' or 'cdn.contentful.com',
  accessToken: process.env.CTFL_TOKEN, // must be a preview token if host = 'preview.contentful.com' and delivery token if 'cdn.contentful.com'
});

// example experience type id
const experienceTypeId = 'article';

export const getServerSideProps = async ({
  params,
  locale,
}: GetServerSidePropsContext) => {
  const currentLocale = (locale || params?.locale) as string;
  const slug = params?.slug as string; // slug in this example, but devs can choose their own way

  try {
     const experienceEntry = await fetchers.fetchExperienceEntry({
       client,
       experienceTypeId,
       locale: currentLocale,
       identifier: {
         slug,
       },
     });

     if (!experienceEntry) {
       // handle 404 case
     }

     const { entries, assets } = await fetchers.fetchReferencedEntities({
       client,
       experienceEntry,
       locale: currentLocale,
     });

     return {
       props: {
         experienceEntry,
         referencedAssets: assets,
         referencedEntries: entries,
         slug,
         locale: currentLocale,
       },
     };
  } catch (e) {
    // handle error
  }
};

function ExperienceBuilderPage({
  experienceEntry,
  referencedAssets,
  referencedEntries,
  slug,
  locale,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {

  const experience = createExperience({
    experienceEntry,
    referencedAssets,
    referencedEntries,
    locale,
    mode: mode as ExternalSDKMode,
  });

  if (!slug || !experienceEntry) {
    return <p>404 Page not found</p>;
  }

  return (
    <main style={{ width: '100%' }}>
      <ExperienceRoot experience={experience} locale={locale} />
    </main>
  );
}

export default ExperienceBuilderPage;
// gatsby-node.js
const path = require('path');
const { fetchers } = require('@contentful/experience-builder');
import { createClient } from 'contentful'

const mode = process.env.GATSBY_IS_PREVIEW === 'true' ? 'preview' : 'delivery';

// Supported values: 'preview.contentful.com' or 'cdn.contentful.com'
const host = mode === 'preview' ? 'preview.contentful.com' : 'cdn.contentful.com';

const client = createClient({
  space: process.env.CTFL_SPACE_ID,
  environment: process.env.CTFL_ENV_ID,
  host, 
  // needs to be preview token if host = 'preview.contentful.com' and delivery token if 'cdn.contentful.com'
  accessToken: process.env.CTFL_TOKEN
});

exports.createPages = async ({ graphql, actions, reporter }) => {
   const { createPage } = actions;

   // assuming that your experience type id is "experience"
   const response = await graphql(`
      allContentfulExperience {
         nodes {
            contentful_id
            slug
            title
            node_locale
            sys {
               contentType {
                  sys {
                     id
                  }
               }
            }
         }
      }
   `);

   if (response.errors) {
      // handle errors
   }

   const { nodes } = response.data.allContentfulExperience;

   for (const node of nodes) {
      const { slug, title, node_locale: localeCode, contentful_id, sys } = node;

      // we support only support CDA/CPA response format, hence we can not use the data we fetched via graphql
      const experienceEntry = await fetchers.fetchExperienceEntry({
         client,
         experienceTypeId: sys.contentType.sys.id
         locale: localeCode,
         identifier: {
           id: contentful_id
         }
      });

      const { entries, assets } = await fetchers.fetchReferencedEntities({
         client,
         experienceEntry,
         locale: localeCode
      });

      createPage({
         path: `/experience/${slug}`,
         component: path.resolve('src/templates/experiencePage.js'),
         context: {
            title,
            expMode: mode,
            experienceEntry,
            referencedEntries: entries,
            referencedAssets: assets,
            locale: localeCode
         }
      });
   }
};
// src/templates/experiencePage.js
import React from 'react'
import {
  createExperience,
  ExperienceRoot,
  defineComponents
} from '@contentful/experience-builder'

// definition can be seen at the top of this file
import { MyButton } from './components/MyButton';

const ExperiencePage = ({ pageContext }) => {
  const { settings, experience, defineComponents } = createExperience({
    experienceEntry: pageContext.experienceEntry,
    referencedEntries: pageContext.referencedEntries,
    referencedAssets: pageContext.referencedAssets,
    locale: pageContext.locale,
    /**
     * Supported values 'preview' or 'delivery'
     * 'preview' mode fetches and renders unpublished data from CPA. Automatically supports canvas interactions if opened on canvas from Contentful's web app
     * 'delivery' mode fetches and renders published data from CDA
     *
     * you have the flexibility to define your own logic to determine the mode in which you want to run your website (for example: depending on the query parameter / hardcoded for a specific deployed instance of the website / env variable)
     */
    mode: pageContext.expMode
  })

  return (
    <ExperienceRoot
      experience={experience}
      // The locale that will appear on the website first
      locale={pageContext.locale}
    />
  );
}
import React, { useEffect, useRef } from 'react'
import {
  useFetchExperience,
  ExperienceRoot
} from '@contentful/experience-builder'

// we surfaced the client as the first step to give you more freedom over data fetching, which we are currently exploring
import { createClient } from 'contentful'

const client = createClient({
  space: 'YOUR_SPACE_ID',
  environment: 'YOUR_ENV_ID',
  // needs to be preview token if host = 'preview.contentful.com' and delivery token if 'cdn.contentful.com'
  accessToken: 'YOUR_ACCESS_TOKEN',
  // optional. Set it to 'preview.contentful.com' in "preview" mode. Uses cdn.contentful.com by default
  host: 'preview.contentful.com'
});

const App = () => {
  // replace with your router;
  const router = {};

  const locale = router.query.locale;
  const slug = router.query.slug;

  const { fetchBySlug, fetchById, experience, isFetching } = useFetchExperience({
    client,
    // id of the experience type (content type)
    experienceTypeId: process.env.REACT_APP_CTFL_EXPERIENCE_TYPE_ID,
    // can be 'preview' or 'delivery'. By default - 'delivery'
    // use 'preview' mode to see the draft content rendered on the page you've built AND to automatically enable canvas functionality when open from the Contentful web app
    // use 'delivery' mode to see the published content rendered on the page you've built. Delivery mode does not enable canvas functionality.
    mode: 'preview', // we use "preview" mode in this example to enable canvas interactions in Contentful web app and be able to build our experience
  })

  useEffect(() => {
     const asyncFetch = async () => {
        try {
           const experience = await fetchBySlug({ slug, experienceTypeId, locale });
           if (!experience) {
             // handle 404 case
           }
        } catch (e) {
          // handle the errors
        }
     };

     if (!experience && isFirstAttempt.current) {
        asyncFetch();
        isFirstAttempt.current = true;
     }
  }, [experience, slug, experienceTypeId, locale, fetchBySlug]);

  if (!experience) {
    return null;
  }

  return (
    <ExperienceRoot
      experience={experience}
      // The locale that appears on the website first
      // You could nicely tie it to the useParam() from router or intenral state or locale manager
      // this value - en-US here is provided as an example reference
      locale="en-US"
    />
  )
}

export default App

ExperienceRoot marks the place which users will be able to edit using the Experience Builder. With ExperienceRoot, you have the flexibility to enable users to build entire pages or only allow them to modify a specific portion of the web page.

Register custom components

After the setup in Contentful is completed, once you open the experience entry in the Contentful web app, you can see the canvas view with your page rendered in the iframe.

In this step, let’s register some custom components to further use them in the Experience Builder.

Registering a component consists of the following steps:

  • Importing a component from the source.
  • Writing a component definition - a JSON object that Contentful uses to pass necessary properties to the component.
  • Passing the component definition to the SDK via a provided function.
Important: The source code of each registered custom component stays in your application and nothing besides the component definition (a JSON object) is sent to Contentful over postMessage. Any change made to the component source code has immediate effect on its representation in the Experience Builder.

To explain the process of registering a custom component, let’s use “Image” component as an example.

Experience builder image component

Image component renders an image with some additional options for the user:

  • Customize the visual properties like Width, Height, Alternative text - that are displayed in the Design tab, because they only provide visual adjustments.
  • Specify the main content for this component - image url that is the data that is required for the component to work. It is rendered in the Content tab. To learn more about the Design and Content tabs, refer to the Experience Builder best practices.

To import an “Image” component from the source, we are going to run the code below.

You can use the code below as a template or use your own component.

To import “Image” component, we add a new folder components and write the following code in components/Image file:

 // src/components/Image.tsx

import React from 'react';

type ImageProps = {
  imageUrl: string;
  className?: string;
  width?: number;
  height?: number;
  altText?: string;
}

export const Image = ({
  imageUrl,
  width = 500,
  height = 500,
  altText,
  className,
  ...ctflProps,
}: ImageProps) => {
  return (
    <img
      className={className}
      src={imageUrl}
      alt={altText}
      width={width}
      height={height}
      {...ctflProps}
    />
  );
}

As you can see from the code, this component supports the following properties:

  • imageUrl
  • width
  • height
  • altText
  • className
  • …ctflProps, which enables Contentful to set additional properties to handle canvas interactions:
    • onMouseDown
    • data-cf-node-id
    • data-cf-node-block-id
    • data-cf-node-block-type

As a next step, we write the “Image” component definition by writing a JSON object with component properties and instructions for each of them. You can learn more about it from our SDK's wiki page

In our example, we store “Image” component definition in the componentDefinitions folder.

Experience builder component definitions folder

The complete component definition for the "Image" component is as follows:

// src/componentDefinitions/image.ts
import type { ComponentDefinition } from '@contentful/experience-builder';

export const imageComponentDefinition: ComponentDefinition = {
  // id of the component. It needs to be unique
  id: 'Image',
  // user friendly name of the component
  name: 'Image',
  // arbitrary string. Components with the same category will be grouped in the web app within the same expand/collapse group
  category: 'Components',
  variables: {
    // each key in the variables object needs to match the prop name of the component
    imageUrl: {
      // user friendly name of the variable
      displayName: 'ImageUrl',
      type: 'Text',
      defaultValue: 'https://picsum.photos/500',
    },
    width: {
      displayName: 'Width',
      type: 'Number',
      defaultValue: 500,
      // makes this variable appear only on Design tab. Disables ability to apply binding. This means that this variable is a purely visual customisation of the component and the value for it shouldn't be stored in entries/assets within Contentful
      group: 'style'
    },
    height: {
      displayName: 'Height',
      type: 'Number',
      defaultValue: 500,
      group: 'style',
    },
    altText: {
      type: 'Text',
      displayName: 'Alt Text',
      defaultValue: 'value2',
      group: 'style',
      // restricts the values to the pre-defined list of options
      validations: {
        in: [
          { value: 'value1', displayName: 'Parrots' },
          { value: 'value2', displayName: 'Alpaca' }
        ]
      }
    }
  }
}
Important: You have freedom to define which portion of the functionality supported by your component you would like to make available for your users. You may want to start small and only expose some of your component’s most important properties.

We can registered our component Image component with the help of the defineComponents function which can be imported from the SDK directly or is also returned by the useExperienceBuilder hook.

Next, we pass the “Image” component to our SDK with the help of the defineComponents function which can be imported from the SDK directly or returned by the useExperienceBuilder hook.

The code below includes all the steps of the Experience Builder project setup.

import React, { useEffect, useRef } from 'react'
import {
  useFetchExperience,
  defineComponents,
  ExperienceRoot
} from '@contentful/experience-builder'

// we surfaced the client as the first step to give you more freedom over data fetching, which we are currently exploring
import { createClient } from 'contentful'

const client = createClient({
  space: 'YOUR_SPACE_ID',
  environment: 'YOUR_ENV_ID',
  // needs to be preview token if host = 'preview.contentful.com' and delivery token if 'cdn.contentful.com'
  accessToken: 'YOUR_ACCESS_TOKEN',
  // optional. Set it to 'preview.contentful.com' in "preview" mode. Uses cdn.contentful.com by default
  host: 'preview.contentful.com'
});

// components, which we are going to register
import { Image } from './components/Image';
import { Button } from './components/Button';

// and their component definitions, which we have written in the previous step
import {
  imageComponentDefinition,
  buttonComponentDefinition
} from './componentDefinitions'

defineComponents([
  { component: Image, definition: imageComponentDefinition },
  { component: Button, definition: buttonComponentDefinition }
]);

const ExperiencePage = () => {
  // replace with your router;
  const router = {};

  const locale = router.query.locale;
  const slug = router.query.slug;

  const { fetchBySlug, fetchById, experience, isFetching } = useFetchExperience({
    client,
    // id of the experience type (content type)
    experienceTypeId: process.env.REACT_APP_CTFL_EXPERIENCE_TYPE_ID,
    // can be 'preview' or 'delivery'. By default - 'delivery'
    // use 'preview' mode to see the draft content rendered on the page you've built AND to automatically enable canvas functionality when open from the Contentful's web app
    // use 'delivery' mode to see the published content rendered on the page you've built. Delivery mode does not enable canvas functionality.
    mode: 'preview', // we use "preview" mode in this example to enable canvas interactions in Contentful web app and be able to build our experience
  })

  useEffect(() => {
     const asyncFetch = async () => {
        try {
           const experience = await fetchBySlug({ slug, experienceTypeId, locale });
           if (!experience) {
             // handle 404 case
           }
        } catch (e) {
          // handle the errors
        }
     };

     if (!experience && isFirstAttempt.current) {
        asyncFetch();
        isFirstAttempt.current = true;
     }
  }, [experience, slug, experienceTypeId, locale, fetchBySlug]);

  if (!experience) {
    return null;
  }

  return (
    <ExperienceRoot
      experience={experience}
      // The locale that will appear on the website first
      // You could nicely tie it to the useParam() from router or intenral state or locale manager
      // this value - en-US here is provided as an example reference
      locale="en-US"
    />
  )
}

export default ExperiencePage

After completing this step, you can go to the Experience Builder in the Contentful web app to see your registered components in the sidebar.

Repeat the process described above to register more of your components.

Enable built-in styles

You can enable ‌built-in-style widgets that are out-of-the-box supported in containers for your custom components.

To enable built-in styles, add builtInStyles attribute to the component definition as in the following example:

import type { ComponentDefinition } from '@contentful/experience-builder'

export const subheadingComponentDefinition: ComponentDefinition = {
  id: 'subheading',
  name: 'Subheading',
  category: 'Atoms',
  builtInStlyes: ['cfMargin', 'cfPadding', 'cfBackgroundColor'],
  variables: {
    text: {
      displayName: 'Text',
      type: 'Text',
      defaultValue: 'Subheading',
    },
    variant: {
      displayName: 'Variant',
      type: 'Text',
      defaultValue: 'dark',
      group: 'style',
      validations: {
         in: [{ value: 'light' }, { value: 'dark' }]
      }
    }
  }
}

In the example above, by including the builtInStyles attribute in the definition we enable the following built-in styles:

  • Margin
  • Padding
  • Background

These built-in styles are displayed as options in the Design tab for a "Subheading" component. Once we select the desired styles, they are applied to the selected component:

Experience builder built-in styles applied

Important:
  • Margin widget is by default enabled for all components. You can set builtInStyles = [] to disable it.
  • The component needs to accept className properly for this feature to work. Otherwise, changes in the “Design” tab have no effect on the component.