Skip to content

Integrations — Rendering Content Hub content directly via GraphQL

Created:
Updated:

Sitecore Content Hub is often the source of truth for:

On XM Cloud projects, I usually see two realistic choices:

  1. Sync content into XM Cloud (for example via Connect or custom ETL) and render from Experience Edge.
  2. Render directly from Content Hub using its preview and delivery GraphQL APIs.

This post focuses on the second approach—no sync—and shows how I:

At architecture level, the setup looks like this:

Content Hub

GraphQL preview and delivery

Next.js App Router

server components

Experience Edge or XM Cloud

for other content

Web and app users


How I decide when “no sync” is a good fit

Rendering directly from Content Hub makes sense for me when:

It might not be ideal if:


How I model Content Hub entities and taxonomies for direct use

A “no sync” architecture leans heavily on clean modeling inside Content Hub.

Define entities and relationships

In Content Hub I:

I like to work backwards from my Next.js user experience:

Design GraphQL-friendly schemas

Content Hub’s GraphQL layer exposes entities with:

I try to design entities so that:


How I enable Content Hub Edge in a minimal way

When I only need a simple “no sync” setup, I keep the enablement steps short and focused:

That is usually enough to start calling Content Hub Edge from a headless Next.js site without bringing in all of the more advanced configuration from the bigger projects.


How I connect Next.js App Router to Content Hub GraphQL

With modeling in place, I wire my headless front end to Content Hub.

Configure endpoints and keys

I typically have:

In the Next.js app I:

For a small project my .env.local often looks like this:

CONTENT_HUB_BASE_URL="https://my-content-hub.cloud"

CONTENT_HUB_PREVIEW_ENDPOINT="${CONTENT_HUB_BASE_URL}/api/graphql/preview/v1"
CONTENT_HUB_DELIVERY_ENDPOINT="https://edge.sitecorecloud.io/api/graphql/v1"

CONTENT_HUB_PREVIEW_TOKEN="preview-api-key-from-content-hub"
CONTENT_HUB_DELIVERY_TOKEN="delivery-api-key-from-content-hub"

Then I add a tiny server-only client in lib/content-hub-client.ts:

// lib/content-hub-client.ts
import 'server-only'

type Mode = 'preview' | 'delivery'

const endpoints: Record<Mode, string | undefined> = {
  preview: process.env.CONTENT_HUB_PREVIEW_ENDPOINT,
  delivery: process.env.CONTENT_HUB_DELIVERY_ENDPOINT,
}

const tokens: Record<Mode, string | undefined> = {
  preview: process.env.CONTENT_HUB_PREVIEW_TOKEN,
  delivery: process.env.CONTENT_HUB_DELIVERY_TOKEN,
}

export async function contentHubFetch<T>(
  query: string,
  {
    variables,
    mode = 'delivery',
  }: { variables?: Record<string, unknown>; mode?: Mode } = {},
): Promise<T> {
  const endpoint = endpoints[mode]
  const token = tokens[mode]

  if (!endpoint || !token) {
    throw new Error(`Content Hub ${mode} endpoint or token is not configured`)
  }

  const res = await fetch(endpoint, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      accept: 'application/json',
      'X-GQL-Token': token,
    },
    body: JSON.stringify({ query, variables }),
    cache: mode === 'preview' ? 'no-store' : 'force-cache',
  })

  if (!res.ok) {
    throw new Error(`Content Hub ${mode} request failed: ${res.status}`)
  }

  const json = (await res.json()) as { data?: T; errors?: { message?: string }[] }
  if (json.errors?.length) {
    throw new Error(json.errors.map((e) => e.message).join('; '))
  }
  if (!json.data) {
    throw new Error('Content Hub response did not contain data')
  }

  return json.data
}

Implement data fetching in App Router routes

For each route, for example app/[locale]/products/[slug]/page.tsx, I:

I use:

Here is a minimal example of a server component that lists blog articles from Content Hub using the client above:

// app/blog/page.tsx
import { contentHubFetch } from '@/lib/content-hub-client'

type BlogListData = {
  blogs: {
    results: {
      id: string
      blog_Title?: string | null
      blog_Slug?: string | null
      blog_Intro?: string | null
    }[]
  }
}

const BLOG_LIST_QUERY = /* GraphQL */ `
  query BlogList($first: Int!) {
    blogs: allM_Content_Blog(first: $first, orderBy: CREATEDON_DESC) {
      results {
        id
        blog_Title
        blog_Slug
        blog_Intro
      }
    }
  }
`

export default async function BlogIndexPage() {
  const data = await contentHubFetch<BlogListData>(BLOG_LIST_QUERY, {
    variables: { first: 10 },
    mode: 'delivery',
  })

  return (
    <main className="mx-auto max-w-4xl px-4 py-10">
      <h1 className="mb-6 text-3xl font-semibold">Blog</h1>
      <ul className="space-y-4">
        {data.blogs.results.map((blog) => (
          <li key={blog.id} className="border-b pb-4">
            <a href={`/blog/${blog.blog_Slug ?? blog.id}`} className="text-xl font-medium hover:underline">
              {blog.blog_Title ?? 'Untitled'}
            </a>
            {blog.blog_Intro && (
              <p className="mt-2 text-neutral-700">{blog.blog_Intro}</p>
            )}
          </li>
        ))}
      </ul>
    </main>
  )
}

For preview, I keep the exact same query and components but call contentHubFetch with mode: 'preview' from draft-mode routes so editors can see unpublished changes.


How I handle preview vs delivery and cache invalidation

Editors need to see their changes quickly; users need fast, cacheable responses.

Preview flows

For preview I:

I connect preview URLs from:

Delivery and invalidation

For delivery I:

I use Content Hub webhooks (or Sitecore Connect) to:


How I integrate with Sitecore Search and XM Cloud

Even when Content Hub is the primary source, XM Cloud and Sitecore Search still play roles.

Site-level composition with XM Cloud

Patterns I like:

I often:

Search integration

I decide where search is powered:

If I use Sitecore Search I:


How I weigh pros and cons versus syncing into XM Cloud

Benefits of no sync:

Drawbacks:

In many cases, a hybrid approach is best for me:


Operational considerations I keep in mind

I try to treat this architecture as an operational system:

With these patterns in place, I can build clean composable sites that leverage Content Hub as a first-class headless CMS, while XM Cloud continues to handle layout, orchestration, and other content domains.



Previous Post
Integrations — Rendering Content Hub content directly via GraphQL
Next Post
Integrations — Streaming XM Cloud forms and events into Salesforce with Sitecore Connect