Getting Started with Optimizely SaaS Core and Next.js Integration: Creating Content Pages

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.

Leave a comment