Sitecore Content Hub is often the source of truth for:
- product content and specs,
- campaign and editorial content,
- taxonomies and relationships,
- and all of the related DAM assets.
On XM Cloud projects, I usually see two realistic choices:
- Sync content into XM Cloud (for example via Connect or custom ETL) and render from Experience Edge.
- 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:
- model entities and taxonomies for direct consumption,
- query Content Hub with GraphQL from Next.js,
- handle preview vs delivery, caching, and invalidation,
- integrate with Sitecore Search and XM Cloud where needed.
At architecture level, the setup looks like this:
How I decide when “no sync” is a good fit
Rendering directly from Content Hub makes sense for me when:
- Content Hub is already the primary CMS for a content domain (for example global product catalog),
- XM Cloud is used mainly for experience composition or other parts of the site,
- you want to avoid duplication and drift between two content stores,
- you have a headless front end comfortable with calling multiple APIs.
It might not be ideal if:
- editors strongly prefer a single authoring surface,
- you rely heavily on XM Cloud-specific features for that content (for example specific personalization options),
- or your organization isn’t ready to manage two content endpoints in a single app.
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:
- model content as entities (for example
Article,Product,Category,Author), - use relations to represent hierarchies and links (for example product → category, article → author),
- use taxonomies to capture facets and navigation.
I like to work backwards from my Next.js user experience:
- what pages and components need to render,
- what fields and relationships they depend on,
- which filters and sorts are required (by category, brand, locale, etc.).
Design GraphQL-friendly schemas
Content Hub’s GraphQL layer exposes entities with:
- types, fields, and relationships,
- filter and sorting parameters,
- pagination controls.
I try to design entities so that:
- key queries (for example “article by slug”, “products by category”) are straightforward,
- field names are consistent and predictable,
- you can filter efficiently by locale, status, and taxonomy.
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:
- In Content Hub I go to
Manage → Settings → Publishing → PublishingSettingsand make sure publishing to Experience Edge is enabled for the tenant. - Under
Manage → Delivery platform, I turn on publishing for the entity definitions I care about (for exampleM.Content_BlogorM.Product) and add simple conditions like “Status is Final or Approved”. - I check the Experience Edge for Content Hub docs to confirm:
- Preview endpoint:
https://<content-hub-host>/api/graphql/preview/v1 - Delivery endpoint:
https://edge.sitecorecloud.io/api/graphql/v1
- Preview endpoint:
- I create two API keys (Preview and Delivery) in
Manage → API Keysand note them down once — they will become theX-GQL-Tokenvalues in my Next.js app.
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:
- a preview endpoint (for editors to see drafts and in-review content),
- a delivery endpoint (for published, publicly available content).
In the Next.js app I:
- store GraphQL endpoints and keys in
.envfiles, - expose them via a configuration module (for example
lib/contentHubClient.ts), - ensure preview keys never leak to the browser.
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:
- parse
localeandslug, - call Content Hub GraphQL with a query like
productBySlug(locale, slug), - map the result into view models expected by my components.
I use:
generateStaticParamsplus incremental static regeneration (ISR) for high-traffic pages,fetchor a dedicated client with caching hints,- proper error handling and fallback states.
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:
- use Next.js draft mode or a preview token on routes,
- call Content Hub preview GraphQL endpoint,
- bypass heavy caching,
- surface status badges (for example “Draft” or “In review”) in the UI.
I connect preview URLs from:
- Content Hub (for example buttons or actions),
- or from XM Cloud pages if I embed these headless sections inside layout experiences.
Delivery and invalidation
For delivery I:
- build pages using ISR or server-side rendering with caching,
- pull from the delivery endpoint,
- keep time-to-live values appropriate to content freshness needs.
I use Content Hub webhooks (or Sitecore Connect) to:
- trigger revalidation of specific routes when content changes,
- or send a message to a queue that the Next.js backend listens to for invalidations.
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:
- XM Cloud owns navigation, layout, and some content types,
- Content Hub provides specific slices (for example product catalog, blog),
- the Next.js head calls both Experience Edge and Content Hub GraphQL as needed.
I often:
- use XM Cloud to route users to sections served primarily by Content Hub,
- embed React components that call Content Hub directly inside XM Cloud pages.
Search integration
I decide where search is powered:
- Sitecore Search indexing Content Hub content via connectors,
- or Content Hub’s own search endpoints if that is sufficient.
If I use Sitecore Search I:
- index Content Hub entities with enough fields to support keyword and facet searches,
- store URLs or identifiers that the front end uses to fetch full details from Content Hub or XM Cloud.
How I weigh pros and cons versus syncing into XM Cloud
Benefits of no sync:
- a single source of truth for Content Hub-managed content,
- no duplication and drift between systems,
- leaner integrations (fewer ETL jobs and sync conflicts).
Drawbacks:
- more complex front-end data fetching (multiple APIs to coordinate),
- more endpoints and keys to manage,
- potentially different editorial experiences for different content types.
In many cases, a hybrid approach is best for me:
- content that needs heavy personalization or XM Cloud-specific features is synced or managed there,
- other content (especially cross-channel product or campaign content) is served directly from Content Hub.
Operational considerations I keep in mind
I try to treat this architecture as an operational system:
- monitor GraphQL performance and error rates,
- track which queries and entities are most used,
- keep an eye on API quotas and rate limits,
- document fallback behaviors (for example “show limited details if Content Hub is unavailable”).
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.
Useful links
- Experience Edge for Content Hub APIs — https://doc.sitecore.com/ch/en/developers/cloud-dev/experience-edge-for-content-hub-apis.html
- Configure publishing settings in Content Hub — https://doc.sitecore.com/ch/en/users/content-hub/configure-the-publishing-settings.html
- Preview and delivery GraphQL APIs — https://doc.sitecore.com/ch/en/developers/cloud-dev/preview-api.html
- Next.js data fetching and caching — https://nextjs.org/docs/app/getting-started/caching-and-revalidating