Content Hub + XM Cloud Content Supply Chain — A Practical Integration Map

The integration diagram I inherited had one arrow, labeled "sync," pointing from Content Hub into XM Cloud. Under the arrow was a year's worth of duplicated blogs: a CMP entity, a matching page item, a broken link where the body had been updated on one side only, and two workflows that both believed they were the approver. The teams did not need a better sync. They needed a map of which system is allowed to own which decision, because Content Hub and the SaaS CMS are still separate applications even after the SitecoreAI packaging.
Sitecore's own FAQ says Content Hub is part of the SitecoreAI entitlement and is provisioned on request. It is not stood up automatically when an XM Cloud organization is upgraded. I still meet teams who assume the tenant "includes Hub" and then discover there is no instance to point a connector at. This is the map I draw once the instance actually exists. I use the XM Cloud name and the SitecoreAI name interchangeably only when I have said which portal screen I mean. The connector docs still say XM Cloud. The head still reads Experience Edge.
Three ways out of Content Hub, and only one of them is "make items"
Content Hub's accelerate guidance splits distribution into publish, push, and pull, and it recommends publish to Experience Edge for Content Hub as the default. Edge gives you the CDN and the content filters already configured in Hub. Push and pull remain tools for a specific consumer. They are not the first design.
The CMP connector, Sitecore Connect for Content Hub, is the path that creates items in XM Cloud (or in XM/XP) and keeps them aligned with Hub entities. The same guidance limits that path to a clear condition: use it when the approval workflow for that content lives entirely in the XM Cloud environment. If Hub is where legal approves the body, mirroring the entity into an item splits the approval in half. I have done that split. The authors hate both UIs, and the developers hate the reconciliation job. For everything else, the head reads the published Hub content from Edge and XM Cloud stores a reference, not a copy.
The DAM connector is a third path and it is easy to confuse with CMP. DAM is how an author, inside the page builder, browses approved assets and inserts one on a field. It does not migrate the asset binary into the Sitecore media library as the system of record. The asset stays in Hub. The page stores the link the connector created. That is the right tool when a marketer is placing a hero and the wrong tool when a catalog of ten thousand product images needs to render without anyone opening the page builder.
What I configure for DAM, and what I leave out of the repo
The documented setup, which several public write-ups repeat because the product screens are fiddly, is an OAuth client in Content Hub using client credentials, a dedicated Hub user for that client, and environment variables on the CM role in the Deploy app. The variable set enables the connector, points at the Hub URI, carries the client credentials and the user, and names the search page that Browse should open, typically an approved-assets page. A redirect key ties the iframe back to the page the editor expects. CORS on the Hub side has to allow the page builder origin and the environment host. None of this takes effect until the environment is deployed again.
I do not commit those connection strings. They are Deploy app variables, targeted at CM, different per environment, and full of secrets. The repo gets a checklist of the variable names and a note that damEnabled stays off until the OAuth client exists. A sample value with a real client secret is how last year's incident started for a team I cleaned up after. A sample value with placeholders is fine in an internal runbook and useless in a public blog, so here you get the shape and not the keys.
After deploy, the test is manual and specific. Open the page builder, edit an image field, and confirm a Content Hub tab lists approved assets. If the tab is missing, the flag did not reach CM. If the tab is empty, the search page or the user's permissions are wrong. If the browser blocks the iframe, CORS or the third-party cookie policy is wrong. I fix the one that the network panel names. I do not toggle all four in one afternoon and then call it stable.
Public links and the head
For a site that renders Hub assets without a person picking each one, I use public links. A public link is an entity, served from Hub's CDN, and it can be retrieved through Experience Edge. Sitecore's distribution notes make a point I have learned the expensive way: the URL is stable only after the production domain of the tenant is final. Public links created on a temporary hostname get baked into pages, emails, and other CMSs. Changing the domain later is a migration, not a configuration tweak.
The head should prefer that public URL and refuse to call the authenticated Hub API from the browser. A delivery API key belongs on the server. An asset that needs authentication is not a public page asset. If the design needs a transformation, Hub can apply it when the public link is created. I do not re-encode the binary in Next.js on every request.
When the body copy itself lives in Hub, the page item in XM Cloud holds the identifier and the presentation. The component asks Edge for Content Hub, server-side, for that identifier.
type HubArticle = { id: string; title?: string | null };
export async function loadHubArticle(id: string, apiKey: string): Promise<HubArticle> {
// Root field follows the published schema. M.Content.Blog is exposed as allM_Content_Blog.
// Predicate argument names are generated too. Confirm both in the Delivery IDE.
const query = `
query Article($id: String!) {
allM_Content_Blog(where: { id_eq: $id }) {
results {
id
title
}
}
}
`;
const response = await fetch("https://edge.sitecorecloud.io/api/graphql/v1", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-GQL-Token": apiKey,
},
body: JSON.stringify({ query, variables: { id } }),
});
if (!response.ok) {
throw new Error(`Content Hub Edge query failed: ${response.status}`);
}
const payload = (await response.json()) as {
data?: { allM_Content_Blog?: { results?: HubArticle[] } };
};
const match = payload.data?.allM_Content_Blog?.results?.[0];
if (!match) {
throw new Error(`No published Content Hub entity for ${id}`);
}
return match;
}
The header is the one Content Hub's Delivery API documents: X-GQL-Token. The endpoint is https://edge.sitecorecloud.io/api/graphql/v1, and a sandbox tenant uses the beta host instead. The root field is generated from the entity definition, so a generic content(id:) query is the wrong thing to copy from a sample. I treat id_eq the same way: it is the predicate shape to verify in the IDE, not a promise that every definition spells the argument identically. The part I will stand behind is the decision around the call: the server holds the key, the component receives published fields, and a missing entity fails that component instead of falling back to a stale XM Cloud copy we forgot to delete.
Caching follows the page, not the Hub UI. If the article is published in Hub and the XM Cloud page only stores the id, invalidating the XM Cloud page is not enough when the HTML of the article was cached inside the component's data cache. Tag that fetch with the Hub id and invalidate it from the Hub webhook, or set a TTL the editors have agreed to. Two publish buttons means two invalidation paths. The map should say so.
Brand kits are not a connector
The brand-aware Content Hub setup from 31 October lives entirely in Hub and Stream. It changes how copy is drafted. It does not deliver the copy to the site. A mapped brand kit will not appear as a datasource in the page builder, and the DAM connector will not start enforcing tone. I keep that work on its own line in the estimate. The supply chain is allowed to assume the body was written on brand. It is not the thing that makes the body on brand.
Search is a consumer, not a fourth connector. If Sitecore Search should index Hub articles, the source is the public URL or the Edge payload, with attributes extracted the way any other source is extracted. Indexing both the Hub URL and the XM Cloud mirror of the same article is how I have produced two search hits and an answer that cites the stale one. One public URL per article. I pick it when I pick the owner.
The map I leave on the wall
| Need | Owner of approval | Integration | What XM Cloud stores |
|---|---|---|---|
| Author picks an approved image while laying out a page | Hub for the asset, XM Cloud for the page | DAM connector | The link the connector writes on the image field |
| Thousands of assets render without a page-builder step | Hub | Public links via Edge | Nothing, or an id the head resolves |
| Long-form body approved in Hub, presented by a headless page | Hub | Edge query from the head | The entity id and the presentation |
| Long-form body that must be approved only in XM Cloud | XM Cloud | CMP connector | The mirrored item, and no parallel Hub approval |
| On-brand drafting | Brand team, in the kit | Brand Assistant | Nothing until a person saves the entity |
I print that table because the word "connector" has been doing too much work. The CMP connector is the rare cell, not the default. Sitecore's cookbook says the same thing in a longer form, and the projects where I ignored it are the projects with the reconciliation jobs.
The order I connect a new pair of tenants
- Confirm the Content Hub instance exists and whether this program was entitled to one. "It's in the SitecoreAI bundle" is not the same as provisioned.
- Decide, per content type, which column of the table applies. Write down the approver before you write down the connector.
- For author-picked assets, create the OAuth client, set the CM variables, set CORS, redeploy, and test one image field.
- For head-resolved content, publish to Edge, query it server-side, and tag the cache by Hub id.
- Only then, and only for types whose workflow truly lives in XM Cloud, turn on CMP and delete any plan to approve the same entity in both places.
A supply chain is a set of ownership decisions that the APIs happen to allow. The diagram with a single sync arrow is how a team ends up maintaining two sources of truth and calling the drift a bug. I would rather a page item that stores an id and a head that can show me the entity Edge returned. When that entity is wrong, I know which editor to call.

