In our previous blog Getting Started with Optimizely SaaS Core and Next.js Integration, we delved into the exciting realm of integrating Optimizely SaaS Core with Next.js, creating a Start Page with Title and Description properties. Now, let’s take our content rendering to the next level by exploring content areas and blocks within Optimizely CMS.
Setting up ContentArea
Navigate to the Admin view in Optimizely CMS and head to Content Types Settings. Here, we’ll enrich our ContentPage item by adding a new field called MainContentArea. This field will serve as the container for our blocks.

Creating Blocks
Let’s create two custom blocks: HeadingBlock and ContactBlock.
HeadingBlock

ContactBlock

Populating Content
With our blocks ready, it’s time to populate content. Create instances of HeadingBlock and ContactBlock, filling in the relevant fields.
Assign these blocks to the MainContentArea field on the AboutUs page or any relevant page of your choice.



Creating Blocks in Next.js
Heading Block
Now, let’s create the component for the HeadingBlock in Next.js. Create the file /blocks/HeadingBlock.tsx with the following code:
import React from "react";
import apolloClient from "../lib/apolloClient";
import { gql } from "@apollo/client";
interface Props {
id: number;
}
const HeadingBlock = async ({ id }: Props) => {
const client = apolloClient();
var data = await client.query({
query: gql`
query HeadingBlockQuery($id: Int) {
HeadingBlock(where: { ContentLink: { Id: { eq: $id } } }) {
items {
Title
}
}
}
`,
variables: {
id: id,
},
});
var item = data.data.HeadingBlock.items[0];
return (
<div className="mt-4 -mb-3">
<div className="not-prose relative bg-slate-50 rounded-xl overflow-hidden dark:bg-slate-800/25">
<div className="relative rounded-xl overflow-auto p-8">
<div className="relative text-xl text-center font-medium leading-6">
<p className="text-slate-500 dark:text-sky-400">{item.Title}</p>
</div>
</div>
<div className="absolute inset-0 pointer-events-none border border-black/5 rounded-xl dark:border-white/5"></div>
</div>
</div>
);
};
export default HeadingBlock;
This component essentially fetches data from Optimizely CMS via GraphQL based on the provided ContentLink Id and renders the content accordingly.
Contact Block
Similar to the HeadingBlock, let’s create the ContactBlock component in the file /blocks/ContactBlock.tsx with the following code:
import React from "react";
import apolloClient from "../lib/apolloClient";
import { gql } from "@apollo/client";
import { getImageUrl } from "../lib/urlHelper";
interface Props {
id: number;
}
const ContactBlock = async ({ id }: Props) => {
const client = apolloClient();
var data = await client.query({
query: gql`
query ContactBlockQuery($id: Int) {
ContactBlock(where: { ContentLink: { Id: { eq: $id } } }) {
items {
ContactName
Position
Image {
Url
}
}
}
}
`,
variables: {
id: id,
},
});
var item = data.data.ContactBlock.items[0];
var imageUrl = getImageUrl(item.Image.Url);
return (
<div className="mt-4 -mb-3">
<div className="not-prose relative bg-slate-50 rounded-xl overflow-hidden dark:bg-slate-800/25">
<div className="absolute inset-0 bg-grid-slate-100"></div>
<div className="relative rounded-xl overflow-auto p-8">
<div className="overflow-visible relative max-w-sm mx-auto bg-white shadow-lg ring-1 ring-black/5 rounded-xl flex items-center gap-6 dark:bg-slate-800 dark:highlight-white/5">
<img
className="absolute -left-6 w-24 h-24 rounded-full shadow-lg"
src={imageUrl}
/>
<div className="flex flex-col py-5 pl-24">
<strong className="text-slate-900 text-sm font-medium dark:text-slate-200">
{item.ContactName}
</strong>
<span className="text-slate-500 text-sm font-medium dark:text-slate-400">
{item.Position}
</span>
</div>
</div>
</div>
<div className="absolute inset-0 pointer-events-none border border-black/5 rounded-xl dark:border-white/5"></div>
</div>
</div>
);
};
export default ContactBlock;
Helper Class
In addition, it’s essential to create a helper class featuring the getImageUrl method. This method plays a crucial role in fetching the image URL from the CMS. During my testing in Optimizely SaaS, I observed that images sourced from blocks lack the server name. On the other hand, images defined within pages already include the server name.
Let’s create a helper class with the method getImageUrl. This class can be named urlHelper and can be placed in a file like /lib/urlHelper.tsx with the following code:
const getImageUrl = (path = "") => {
const siteUrl = process.env.DXP_URL as string;
if (!path) {
return "";
}
return path.startsWith("http") ? path : siteUrl + path;
};
export { getImageUrl };
Furthermore, make sure to create a variable in your environment file, .env.local (create it if it does not exist), with the following value:
DXP_URL=https://instance-id.cms.optimizely.com
Change instance-id with your own value.
Content Area Factory Class
To streamline the rendering process of various blocks based on their content types, we are going to create a Factory class. The provided class allows for the seamless registration of new blocks, ensuring that additions can be incorporated without the need to modify references to this central factory class. This approach enhances maintainability and extensibility in your application architecture.
Create the file [[...slug]]\ContentAreaFactory.tsx with the following code:
import React from "react";
import HeadingBlock from "../blocks/HeadingBlock";
import ContactBlock from "../blocks/ContactBlock";
interface Props {
contentAreas: any;
}
const ContentAreaFactory = ({ contentAreas }: Props) => {
return (
<>
{contentAreas.map((item: any) => {
var contentTypes = item.ContentLink.Expanded.ContentType;
if (contentTypes.includes("HeadingBlock")) {
return (
<HeadingBlock key={item.ContentLink.Id} id={item.ContentLink.Id} />
);
} else if (contentTypes.includes("ContactBlock")) {
return (
<ContactBlock key={item.ContentLink.Id} id={item.ContentLink.Id} />
);
} else {
return null;
}
})}
</>
);
};
export default ContentAreaFactory;
Adding Content Area Factory to the Pages
The final step in enhancing your content rendering is to integrate the Content Area Factory into the Content Page. Begin by including the new MainContentArea field in the GraphQL query. Afterward, place the <ContentAreaFactory /> component below the Description field in your page structure. Review the following code snippet for a more detailed implementation:
import React from "react";
import apolloClient from "../lib/apolloClient";
import { gql } from "@apollo/client";
import ContentAreaFactory from "../[[...slug]]/ContentAreaFactory";
interface Props {
segment: string;
}
const ContentPage = async ({ segment }: Props) => {
const client = apolloClient();
var data = await client.query({
query: gql`
query MyQuery($segment: String) {
ContentPage(where: { RouteSegment: { eq: $segment } }) {
items {
Title
Description
MainContentArea {
ContentLink {
Id
Expanded {
ContentType
}
}
}
}
}
}
`,
variables: {
segment: segment,
},
});
var page = data.data.ContentPage.items[0];
return (
<>
<h1 className="mb-4 text-4xl font-extrabold">{page.Title}</h1>
<p dangerouslySetInnerHTML={createMarkup(page.Description)} />
<ContentAreaFactory contentAreas={page.MainContentArea} />
</>
);
};
function createMarkup(c: string) {
return { __html: c };
}
export default ContentPage;
By following these steps, you seamlessly integrate the Content Area Factory into your Content Page, allowing for dynamic rendering of different block types without the need for extensive modifications.
