Dev guideRecipesAPI ReferenceChangelog
Dev guideRecipesUser GuidesNuGetDev CommunityOptimizely AcademySubmit a ticketLog In
Dev guide

Quick start guide: Next.js and React

Learn how to connect your GraphQL endpoint, query content, and display Content Management System (SaaS) data using Apollo Client.

Integrate a Next.js project with Optimizely Content Management System (SaaS) through Apollo Client. This guide configures a GraphQL client with cached templates, creates reusable components that fetch content and landing pages, and renders the results in your application. The result is type-safe data access, optimized queries, and a streamlined developer experience for managing content in CMS (SaaS).

Prerequisites

Before you begin, ensure you have the following:

  • Node.js (v18 or higher).
  • An Optimizely CMS (SaaS) instance.
  • API key and Optimizely Graph endpoint.

Create a Next.js project

Scaffold the Next.js app that hosts the Apollo Client and React components.

npx create-next-app@latest my-saas-cms-site
cd my-saas-cms-site

Install GraphQL dependencies

Add the Apollo Client and GraphQL runtime to the project.

npm install @apollo/client graphql

Configure Apollo Client with cached templates

Create the Apollo Client and point it at your Optimizely Graph endpoint with the cg-stored-query header so repeat requests reuse cached templates.

Create a file named lib/apollo-client.ts and add the following.

import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';

const httpLink = new HttpLink({
  uri: 'https://beta.cg.optimizely.com/content/v2?auth=YOUR_ACCESS_KEY&stored=true',
  headers: {
    // Enable cached templates for better performance
    'cg-stored-query': 'template',
  }
});

const client = new ApolloClient({
  link: httpLink,
  cache: new InMemoryCache(),
  defaultOptions: {
    watchQuery: {
      errorPolicy: 'all',
    },
    query: {
      errorPolicy: 'all',
    },
  },
});

export default client;

Configure environment variables

Store the Optimizely Graph endpoint in an environment variable so it stays out of source code and is straightforward to swap per environment.

Create a .env.local file and add the following.

NEXT_PUBLIC_OPTIMIZELY_GRAPH_ENDPOINT=https://beta.cg.optimizely.com/content/v2?auth=YOUR_ACCESS_KEY&stored=true

Create your first component

Build a React component that issues a GetContent query against _Content, renders a list of items, and surfaces loading and error states.

Create a file named components/ContentList.tsx and add the following.

import { gql } from '@apollo/client';
import { useQuery } from '@apollo/client/react';

const GET_CONTENT = gql`
query GetContent($locale: [Locales], $skip: Int = 0, $limit: Int = 10) {
  _Content(
    locale: $locale
    orderBy: { _metadata: { lastModified: DESC } }
    skip: $skip
    limit: $limit
  ) {
    items {
      _metadata {
        displayName
        url {
          default
        }
        types
        published
      }
    }
    total
  }
}
`;

export function ContentList() {
  const { loading, error, data } = useQuery(GET_CONTENT, {
    variables: { locale: ['en', 'NEUTRAL'] },
  });

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      <h1>Latest content from CMS (SaaS)</h1>
      <p>Total content items: {data._Content.total}</p>

      {data._Content.items.map((item, index) => (
        <div
          key={index}
          style={{
            marginBottom: '1rem',
            padding: '1rem',
            border: '1px solid #ccc',
          }}
        >
          <h3>{item._metadata.displayName}</h3>
          <p>
            <strong>Type:</strong> {item._metadata.types?.join(', ')}
          </p>
          <p>
            <strong>URL:</strong> {item._metadata.url?.default}
          </p>
          <p>
            <strong>Published:</strong>{' '}
            {new Date(item._metadata.published).toLocaleDateString()}
          </p>
          {item._metadata.url?.default && (
            <a
              href={item._metadata.url.default}
              target="_blank"
              rel="noopener noreferrer"
            >
              View content →
            </a>
          )}
        </div>
      ))}
    </div>
  );
}

Create a landing page component

Build a second component that targets the LandingPage content type so you can render marketing pages alongside the generic content list.

Create a file named components/LandingPageList.tsx and add the following.

import { gql } from '@apollo/client';
import { useQuery } from '@apollo/client/react';

const GET_LANDING_PAGES = gql`
  query GetLandingPages($locale: Locales) {
    LandingPage(locale: [$locale]) {
      items {
        _metadata {
          displayName
          url {
            default
          }
          published
        }
      }
      total
    }
  }
`;

export function LandingPageList() {
  const { loading, error, data } = useQuery(GET_LANDING_PAGES, {
    variables: { locale: 'en' },
  });

  if (loading) return <p>Loading landing pages...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      <h2>Landing pages</h2>
      <p>Total landing pages: {data.LandingPage.total}</p>
      {data.LandingPage.items.map((page, index) => (
        <div
          key={index}
          style={{
            marginBottom: '1rem',
            padding: '1rem',
            border: '1px solid #ccc',
          }}
        >
          <h3>{page._metadata.displayName}</h3>
          <p>
            <strong>URL:</strong> {page._metadata.url?.default}
          </p>
          <p>
            <strong>Published:</strong>{' '}
            {new Date(page._metadata.published).toLocaleDateString()}
          </p>
          {page._metadata.url?.default && (
            <a
              href={page._metadata.url.default}
              target="_blank"
              rel="noopener noreferrer"
            >
              View page →
            </a>
          )}
        </div>
      ))}
    </div>
  );
}

Use the components in your page

Wrap the app in an ApolloProvider and render the two components on the home page so they share the configured client.

In app/page.tsx, import and render the components.

'use client';
import { ApolloProvider } from '@apollo/client/react';
import client from '../lib/apollo-client';

import { ContentList } from '../components/ContentList';
import { LandingPageList } from '../components/LandingPageList';

export default function Home() {
  return (
    <ApolloProvider client={client}>
      <div style={{ padding: '2rem' }}>
      <h1>Mosey Bank CMS content</h1>
      <ContentList />
      <hr style={{ margin: '2rem 0' }} />
      <LandingPageList />
      </div>
    </ApolloProvider>
  );
}

Run and verify

Start the development server and confirm that Optimizely Graph returns content for the two components.

npm run dev

Open http://localhost:3000/ in your browser to see your content and landing pages loaded from Optimizely Graph.