In our previous blog post Getting Started with Optimizely SaaS Core and Next.js Integration, we explored the process of getting started with Optimizely SaaS Core and Next.js integration, where we created a Start Page with two properties: Title and Description. Now, let’s move forward and create additional page types in Optimizely CMS.
Creating a Content Page in Optimizely CMS
Create a new content type named Content Page
with two properties: Title and Description. This mirrors the structure we set up for the Start Page.
Go to the Edit View and create two pages under the Home item – ‘About Us’ and ‘Company’. Provide values for the title and description fields.
Publish the changes. After a short delay, the new content will be synchronized with Optimizely Graph.
Querying Content with Optimizely Graph
In Optimizely Graph, create a new query to retrieve content based on the route segment. This query will be used later in the code.
Building About Us Page in Next.js
Create Next.js Pages:
- Create the folder
about-us
. - Inside, create the file
page.tsx
and add the following code.
import React from "react";
import apolloClient from "../lib/apolloClient";
import { gql } from "@apollo/client";
const AboutUsPage = async () => {
const client = apolloClient();
var data = await client.query({
query: gql`
query MyQuery($segment: String) {
ContentPage(where: { RouteSegment: { eq: $segment } }) {
items {
Title
Description
}
}
}
`,
variables: {
segment: "about-us",
},
});
var page = data.data.ContentPage.items[0];
return (
<>
<h1 className="mb-4 text-4xl font-extrabold">{page.Title}</h1>
<p dangerouslySetInnerHTML={createMarkup(page.Description)} />
</>
);
};
function createMarkup(c: string) {
return { __html: c };
}
export default AboutUsPage;
By incorporating the segment
parameter in the GraphQL query, you enable dynamic content retrieval based on the route segment, offering a flexible and scalable approach to managing content in your Next.js application.
Verify About Us Page:
Access http://localhost:3000/about-us and ensure the page displays correctly.
Creating Company Page in Next.js
Create Company Folder and File:
- Create the folder
company
. - Inside, create the file
page.tsx
with similar code as the About Us page.
import React from "react";
import apolloClient from "../lib/apolloClient";
import { gql } from "@apollo/client";
const CompanyPage = async () => {
const client = apolloClient();
var data = await client.query({
query: gql`
query MyQuery($segment: String) {
ContentPage(where: { RouteSegment: { eq: $segment } }) {
items {
Title
Description
}
}
}
`,
variables: {
segment: "company",
},
});
var page = data.data.ContentPage.items[0];
return (
<>
<h1 className="mb-4 text-4xl font-extrabold">{page.Title}</h1>
<p dangerouslySetInnerHTML={createMarkup(page.Description)} />
</>
);
};
function createMarkup(c: string) {
return { __html: c };
}
export default CompanyPage;
Verify Page:
Access http://localhost:3000/company and verify that the Company page renders correctly.
Implementing Dynamic Routes for Enhanced Code Reusability
You might notice that creating pages can be a repetitive process, potentially leading to code duplication. To adhere to the DRY (Don’t Repeat Yourself) principle and enhance code maintainability, let’s implement a more efficient solution using dynamic routes.
Creating Dynamic Route Structure
Create a dynamic segment by wrapping a file or folder name in square brackets. Start by creating the folder [[...slug]]
under the app
folder.
app/
└── [[...slug]]
└── page.tsx
Dynamic Route Implementation
Inside the [[...slug]]
folder, create the file page.tsx
with the following code. Additionally, ensure that you delete the existing app/page.tsx
file and the about-us and company
folders.
import React from "react";
import apolloClient from "../lib/apolloClient";
import { gql } from "@apollo/client";
import { notFound } from "next/navigation";
interface Props {
params: { slug: string[] };
}
const DynamicPage = async ({ params: { slug } }: Props) => {
var segment: string = "home";
if (slug) {
segment = slug[slug.length - 1];
}
const client = apolloClient();
var data = await client.query({
query: gql`
query ContentQuery($segment: String) {
Content(where: { RouteSegment: { eq: $segment } }) {
items {
RouteSegment
ContentType
}
}
}
`,
variables: {
segment: segment,
},
});
if (data.data.Content.items.length == 0) {
notFound();
}
var page = data.data.Content.items[0];
if (page.ContentType.includes("StartPage")) {
var data = await client.query({
query: gql`
query MyQuery {
StartPage {
items {
Title
Description
}
}
}
`,
});
var page = data.data.StartPage.items[0];
return (
<>
<h1 className="mb-4 text-4xl font-extrabold">{page.Title}</h1>
<p dangerouslySetInnerHTML={createMarkup(page.Description)} />
</>
);
} else if (page.ContentType.includes("ContentPage")) {
var data = await client.query({
query: gql`
query MyQuery($segment: String) {
ContentPage(where: { RouteSegment: { eq: $segment } }) {
items {
Title
Description
}
}
}
`,
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)} />
</>
);
} else {
notFound();
}
};
function createMarkup(c: string) {
return { __html: c };
}
export default DynamicPage;
By implementing dynamic routes, you create a more scalable solution that avoids duplicating code for each page. The [[...slug]]
structure allows for flexibility in handling various page paths. Now, when you access pages under this dynamic route, the DynamicPage
component will handle content retrieval based on the dynamic slug, providing a cleaner and more maintainable codebase.
Enhancing Code Modularity with a Factory Class
To further improve the code, we can introduce a Factory class. A Factory class is a design pattern that provides an interface for creating objects but leaves the choice of their type to the subclasses, creating instances of classes based on certain conditions. In our context, a Factory class can be utilized to dynamically generate components based on the provided parameters, contributing to better code organization and maintainability.
Implementation of a Factory Class
Let’s create a Factory class for generating dynamic pages based on different types. Replace the existing DynamicPage
component with a Factory class implementation:
// File: app/[[...slug]]/DynamicPageFactory.tsx
import React from "react";
import ContentPage from "../pages/ContentPage";
import StartPage from "../pages/StartPage";
import { notFound } from "next/navigation";
class DynamicPageFactory {
static createPage(segment: string, pageData: any) {
console.log(pageData.ContentType);
if (pageData.ContentType.includes("ContentPage"))
return <ContentPage segment={segment} />;
else if (pageData.ContentType.includes("StartPage")) return <StartPage />;
// Add more cases for additional page types
else return notFound();
}
}
export default DynamicPageFactory;
// File: app/pages/StartPage.tsx
import React from "react";
import apolloClient from "../lib/apolloClient";
import { gql } from "@apollo/client";
const StartPage = async () => {
const client = apolloClient();
var data = await client.query({
query: gql`
query MyQuery {
StartPage {
items {
Title
Description
}
}
}
`,
});
var page = data.data.StartPage.items[0];
return (
<>
<h1 className="mb-4 text-4xl font-extrabold">{page.Title}</h1>
<p dangerouslySetInnerHTML={createMarkup(page.Description)} />
</>
);
};
function createMarkup(c: string) {
return { __html: c };
}
export default StartPage;
// File: app/pages/ContentPage.tsx
import React from "react";
import apolloClient from "../lib/apolloClient";
import { gql } from "@apollo/client";
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
}
}
}
`,
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)} />
</>
);
};
function createMarkup(c: string) {
return { __html: c };
}
export default ContentPage;
Now, in the app/[[...slug]]/page.tsx
file, you can use the Factory class to create the appropriate page component:
import React from "react";
import apolloClient from "../lib/apolloClient";
import { gql } from "@apollo/client";
import { notFound } from "next/navigation";
import DynamicPageFactory from "./DynamicPageFactory";
interface Props {
params: { slug: string[] };
}
const DynamicPage = async ({ params: { slug } }: Props) => {
var segment: string = "home";
if (slug) {
segment = slug[slug.length - 1];
}
const client = apolloClient();
var data = await client.query({
query: gql`
query ContentQuery($segment: String) {
Content(where: { RouteSegment: { eq: $segment } }) {
items {
RouteSegment
ContentType
}
}
}
`,
variables: {
segment: segment,
},
});
if (data.data.Content.items.length == 0) {
notFound();
}
var page = data.data.Content.items[0];
const DynamicComponent = DynamicPageFactory.createPage(segment, page);
return <>{DynamicComponent}</>;
};
export default DynamicPage;
With the introduction of the Factory class, you’ve created a more modular and extensible solution. Adding new page types becomes a matter of extending the Factory class, promoting a clean and maintainable architecture.