Set up Experience Builder
Table of contents
What is Experience Builder?
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:
- Setup in Contentful.
- 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
- Log in to the Contentful web app.
- Select a space and environment in which you would like to set up Experience Builder.
- 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.
- In the Name field, enter a custom name for your experience type. An API identifier is generated based on the name.
- Optional: In the Description field, enter a custom description for your duplicated content type.
- Click Next. A “Setup your preview screen” is displayed.
- 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.
- Click Create Experience Type. Your experience type is created and saved.
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:
- Install the SDK — Install the Experience Builder SDK and import it into your project.
- Configure the SDK — Import components from the React component library of your choice.
- 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.
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.
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.
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.
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' }
]
}
}
}
}
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:
- 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.