Step 6 of 10 (60% complete)
Fetching Content & the CLI Sync
Step Code
The code for this specific step can be found on the following branch:
With content types defined and registered, the next piece is fetching content from Optimizely Graph. The SDK provides a GraphClient that wraps the GraphQL API with typed methods.
Fetching with GraphClient
The most common fetch pattern in this starter uses getContentByPath. The SDK resolves which content lives at a given URL path and returns it fully typed:
import { GraphClient } from '@optimizely/cms-sdk' async function getPageContent(locale: string, slug: string[]) { 'use cache' cacheLife('max') const client = new GraphClient(process.env.OPTIMIZELY_GRAPH_SINGLE_KEY!, { graphUrl: process.env.OPTIMIZELY_GRAPH_URL, }) return client.getContentByPath(`/${locale}/${slug.join('/')}/`) }
The 'use cache' directive is React 19's built-in caching mechanism. But before we talk about caching, it is worth taking a closer look at what getContentByPath actually does — because the strategy inside it is genuinely clever.
How getContentByPath Works Under the Hood
If you look at the SDK source code, getContentByPath does not run a single GraphQL query — it runs two, and the second one is dynamically generated based on what the first one returns.
Step 1 — Metadata query (static): The method first calls getContentMetaData, a lightweight fixed GraphQL query that returns just the content type name and whether DAM (Digital Asset Management) is enabled for that content.
Step 2 — Content query (dynamic): Armed with the content type name, it calls createMultipleContentQuery to construct a second GraphQL query on the fly. This query selects exactly the properties configured on that content type — nothing hardcoded, nothing missing.
/** * Fetches content from the CMS based on the provided path or options. * * If a string is provided, it is treated as a content path. If an object is provided, * it may include both a path and a variation to filter the content. * * @param path - A string representing the content path * @param options - Options to include or exclude variations * * @param contentType - A string representing the content type. If omitted, the method * will try to get the content type name from the CMS. * * @returns An array of all items matching the path and options. Returns an empty array if no content is found. */ async getContentByPath<T = any>( path: string, options?: GraphGetContentOptions, ) { const input: GraphVariables = { ...pathFilter(path, options?.host ?? this.host), // Backwards compatibility: if host is not provided in options, use the client's default host variation: options?.variation, }; // Step 1 - Metadata query const { contentTypeName, damEnabled } = await this.getContentMetaData(input); if (!contentTypeName) { return []; } // Step 2 — Content query const query = createMultipleContentQuery( contentTypeName, damEnabled, this.maxFragmentThreshold, ); const response = (await this.request(query, input)) as ItemsResponse<T>; return response?._Content?.items.map(removeTypePrefix); }
The elegance here is that you never need to write or maintain content-type-specific GraphQL fragments. Add a new property to a content type, push it with the CLI, and the very next call to getContentByPath automatically includes it — no query updates on your side, ever.
This is one of the strongest arguments for using the SDK: one reusable method that permanently stays in sync with your content model.
When this function is called for the same arguments, React returns the cached result instead of making a new network request. cacheLife('max') sets the cache lifetime to 30 days with a 1-year expiry — appropriate for CMS content that only changes when explicitly published.
Keeping CMS in Sync with the CLI
The @optimizely/cms-cli package is what bridges your code and the CMS. The CLI reads your optimizely.config.mjs file, scans your component files, and pushes all contentType() definitions to your CMS instance:
npm run opti-push
This must be run whenever you:
- Add a new content type
- Add, rename, or remove a property on an existing content type
- Change
displayName,localized, ordefaultValueon a property
Note: The CLI also supports a --force flag (npm run opti-push-data-loss) which allows destructive changes like removing fields that have existing content. Use this carefully.
Adding opti-push to CI/CD
One of the best practices for any Optimizely project is to run opti-push automatically as part of your build pipeline. This ensures that your CMS schemas are always in sync with your code — especially important when multiple developers are adding new blocks.
Here is an example GitHub Actions step:
- name: Push content types to Optimizely CMS run: npm run opti-push env: OPTIMIZELY_CMS_CLIENT_ID: ${{ secrets.OPTIMIZELY_CMS_CLIENT_ID }} OPTIMIZELY_CMS_CLIENT_SECRET: ${{ secrets.OPTIMIZELY_CMS_CLIENT_SECRET }} OPTIMIZELY_CMS_HOST: ${{ secrets.OPTIMIZELY_CMS_HOST }}
Place this step before your build step. That way, by the time Next.js starts generating static pages and fetching content, the CMS already has the latest schemas and editors can use any new blocks immediately after deployment.
Have questions? I'm here to help!