Eight Ways In: A Content Hub Reading Reference

Hello everyone, and welcome to another blog! In this blog, we’ll explore how to consume Sitecore Content Hub data using the Content Hub WebClient SDK, HTTP raw endpoints, and search queries, with a specific focus on scenarios involving external components.

Every field you'll meet in Content Hub is one of a handful of shapes — a plain property, an option list, a taxonomy entity, a relation, a count, a search box. Each shape has one or two read methods that actually fit it. Here's how to spot which one you're looking at before you write a query.

The SDK gives you several doors into the same data — a typed query engine, a per-entity property/relation loader, an option-list reader, and a raw REST layer underneath all of it. None of them is wrong, exactly, but each is a good fit for exactly one shape of data and an awkward fit for the rest. The fastest way to pick correctly is to look at what you're trying to read before you look at the SDK's method list — the shape tells you the method, not the other way around.

Identify it in one line

Start here. Match what you're looking at to a row, then jump to that specimen for the code.

Looks likeIt's a…Not this if…
A single value under "Properties," no relation iconDirect Propertythe value is itself a small fixed dropdown → Option list
A short fixed dropdown; stored value looks like a code, not a numeric idDirect Option listit has hundreds of values with their own detail page → Taxonomy
A large controlled vocabulary — each value is its own entityGraph Taxonomy entityyou already hold the entity and just want its linked ids → Relation
A "related items" tab or panel between two entity typesGraph Relationyou don't have either entity yet, you're searching by it → still a relation, just queried instead of loaded
A number next to a label, no list neededComputed Aggregate countthe number is already painted on screen by something else → DOM read
A search-as-you-type box expecting partial matchesEscape hatch Full-text searchan exact match is enough → typed filter instead
No typed SDK method covers what you needEscape hatch Raw RESTa typed method exists → always prefer it
The exact value is already visible in a native widget on the pageLast resort Rendered valuere-fetching it is cheap and you already hold the entity → just re-fetch

The eight specimens

01Direct

Plain property

Spot it

A single value sitting right under an entity's Properties list — text, number, boolean, or date. No relation icon next to it, no separate detail page it links to.

Read it
const config = new EntityLoadConfiguration(
  CultureLoadOption.Default,
  new PropertyLoadOption(['YourPropertyName']),
  RelationLoadOption.None,
);
const entity = await client.entities.getAsync(entityId, config);
const value = entity.getPropertyValue('YourPropertyName', locale);
!

Not every property accepts a locale — culture-insensitive properties throw if you pass one. Try the locale-aware call first, fall back to the plain call on catch, rather than guessing which kind a property is up front.

02Direct

Option list field

Spot it

A short, fixed dropdown — a status, a handful of enum-like states. The stored value reads like a code (FY2627, Approved), not a numeric entity id, and there's no detail page behind it.

Read it
const optionList = await client.optionLists.getAsync('YourOptionListIdentifier');
const values = optionList.getOptionListValues();
// [{ identifier: 'FY2627', labels: { 'en-US': 'FY26/27', ... } }, ...]
!

Store the identifier, display the label. Labels are locale-keyed objects, not plain strings — resolve the current locale with a fallback, the same way you'd resolve a culture-sensitive property.

03Graph

Taxonomy / classification entity

Spot it

A large controlled vocabulary where each value is a full entity with its own id and detail page — not just a code. Could be hundreds or thousands of values, usually reached from content through a relation.

Read it
const filter = new DefinitionQueryFilter({
  operator: ComparisonOperator.Equals,
  name: 'M.YourTaxonomyDefinition',
});
const query = new Query({
  filter, skip: pageIndex * pageSize, take: pageSize,
  sorting: [{ field: 'id', fieldType: SortFieldType.System, order: QuerySortOrder.Asc }],
});
const result = await client.querying.queryAsync(query, loadConfig);
!

There's no standard "display label" property across taxonomy definitions — different ones use different property names for it. Try a short list of common candidates (e.g. a classification-name, a label, a display-name property) and use the first that resolves. Always paginate; never take the whole vocabulary at once.

04Graph

Relation between entities

Spot it

A "related items" tab or panel connecting two entity types. The relation has a name, and it's directional — walked from one side (parent role) it returns a different set than walked from the other side (child role).

Read it — you already hold the entity
const config = new EntityLoadConfiguration(
  CultureLoadOption.Default, PropertyLoadOption.None,
  new RelationLoadOption(['YourRelationName']),
);
const entity = await client.entities.getAsync(entityId, config);
const relatedIds = entity.getRelation('YourRelationName', RelationRole.Child)?.getIds() ?? [];
Read it — you're searching by the relation instead
const filter = new RelationQueryFilter({ relation: 'YourRelationName', parentIds: [knownParentId] });
const results = await client.querying.queryAsync(new Query({ filter }), loadConfig);
!

RelationQueryFilter searches descendants of a known parent — it can't run in reverse. If you already hold the child-side entity and just need its one parent, read the id directly off the entity's own relation instead of trying to search for it; validate with an id-equality filter afterward if it needs to slot into a larger composite filter.

05Computed

Aggregate count

Spot it

A number next to a label or tab — "12 related items" — where you need the count, not the records themselves.

Read it
const query = new Query({ filter: yourFilter, take: 1 });
const result = await client.querying.queryAsync(query, minimalLoadConfig);
const count = result.totalNumberOfResults; // not result.items
!

The engine still computes the full match count even with take: 1 — cheap on transfer, not on computation. Need several counts at once? Fire them in parallel and isolate failures per query (catch → return 0), so one bad filter doesn't take every other count down with it.

06Escape hatch

Full-text search

Spot it

A search-as-you-type box where a partial term should match across a label or name field — not an exact value lookup.

Read it
const query = `Definition.Name=="M.YourDefinition" AND FullText=="${escapedTerm}"`;
const res = await fetch(`/api/entities/query?query=${encodeURIComponent(query)}&take=100`);
!

Try the typed client's own Contains-style property filter first — but verify it actually returns matches before committing to it; it doesn't always. The raw endpoint's FullText== operator is often the one that reliably does partial matching. Always escape user input before it lands inside the query string.

07Escape hatch

Uncovered REST endpoint

Spot it

You need something the typed client has no method for — a relation's raw link list, an endpoint you only confirmed by watching the network tab.

Read it
const res = await client.raw.getAsync(`/api/entities/${id}/relations/YourRelationName`);
// or, with no SDK client in scope at all — the session cookie already carries auth:
const res = await fetch(`/api/entities/${id}/relations/YourRelationName`);
const ids = (res.content?.children ?? []).map(link => extractIdFromHref(link.href));
!

Treat this as the last resort, not the fast path — confirm no typed method covers it first. If you're not certain of the exact relation or endpoint name, trying a short ordered list of candidates and taking the first that returns data is a reasonable, if slightly grim, way to route around not knowing the schema for sure.

08Last resort

Value already rendered on screen

Spot it

The exact value you need is already computed and painted by a native platform widget elsewhere on the page — re-fetching it from Content Hub would just recompute something already visible.

Read it
const el = document.querySelector(yourSelector);
// wait for the widget's own "done" event, then debounce until the text stops changing
const observer = new MutationObserver(() => scheduleSettleCheck(el));
observer.observe(el, { characterData: true, childList: true, subtree: true });
!

Widgets often repaint in steps — an intermediate "0" before the real value — so never trust the first mutation. Debounce until it goes quiet, and tag each read cycle with a generation counter so a newer, overlapping read can discard a stale one instead of racing it into the DOM. This is coupled to markup you don't own — reach for it only when the alternative is a genuinely wasteful re-query.

If more than one fits

Roughly in order of how strong a contract each one gives you — reach down this list only as far as you have to.

1

Typed property or relation load. Scoped by EntityLoadConfiguration, so you only pay for what you actually asked for.

2

Typed query — for taxonomy browsing, relation search, or a count — over a DefinitionQueryFilter / RelationQueryFilter tree.

3

Option list reader for enumerated values — it's a different accessor because it's a different data shape, not a downgrade.

4

Raw REST, once you've confirmed no typed method covers the case — an escape hatch, used deliberately, not a default.

5

Reading a rendered value off the DOM — only when the alternative is recomputing something already on screen, and only with a debounce and a generation token guarding it.

A quick-identification reference for reading Content Hub data by field shape, not by relation or component name.

You can check my other blogs too if interested. Blog Website

Comments

Popular posts from this blog

Switching Docker Desktop to Docker CLI in Easy Way

Sitecore XM Cloud Form Integration with Azure Function as Webhook

Extending Edge GraphQl Schema To Support Custom Fields In XM Cloud