Skip to content

The content model

ebitex Content keeps three questions apart: what a piece of content is, how it looks, and where it lives. Contracts and Components answer the first, Templates and Presentations the second, and the Experience tree the third. Your site’s code mostly deals with the result: a page delivered as JSON, which you render by dispatching on the Template it names.

This page describes the model as a developer meets it. For how editors build it, see the in-app help.

A Contract is a content type: a named, versioned list of fields, such as “Article” or “Author”. Every Contract has an external id, a short URL-safe name like article. The external id is what you use in code, and it appears in every delivered descriptor.

Each field has one type. These are the types offered for new fields:

Type Key Holds
Short Text shortText A string. Can be restricted to a fixed list of allowed values
Number number A number. Can be restricted to allowed values
Boolean boolean true or false
Date date A UTC instant (an RFC 3339 string), with or without a time of day
Component component Another piece of content, either as a reference or inline (see below)
Blob blob An uploaded file or image, delivered with a public url
Presentation presentation A Template applied to some content (see below)
Rich Text richText Markdown with embedded content, delivered as an array of fragments
Category category A category from one of the organization’s taxonomies
Link link A link to a page in the Experience tree or to an external URL

A few older types (longText, url, reference, nested and experienceReference) are no longer offered for new fields but keep working where they are already used. A reference or nested field delivers exactly like a component field limited to one mode.

Any field can carry five independent modifiers. They combine freely.

Modifier Effect on the value
Mandatory The field must have a value
Enumerable The field holds a list of values instead of one
Localizable The field holds a default value plus per-locale overrides. Delivery picks one (see Localization)
Contextual The value belongs to the place the content is used, not to the content itself. A Component can hold a fallback
Personalizable The field holds a default plus variants, each shown to a different audience

In a delivered response, localization and personalization are normally already resolved: you receive one plain value, not the envelope. The exceptions are raw=true, which returns localizable values as { "default": …, "locales": { … } }, and a request without a ctx parameter, which returns personalized values as { "default": …, "variants": [ … ] } for you to evaluate. Both are described in the resolvePath reference.

A Contract can extend one parent Contract. It inherits every field the parent declares and adds its own. It cannot remove or redefine an inherited field. A chain can go several levels deep, such as Web page, then Article, then News article.

An abstract Contract cannot have content of its own. It exists to be extended, and to act as a constraint. A field that allows “any Web page” accepts every Contract that extends Web page, and a Template that supports an abstract Contract can present content of every Contract beneath it.

A Contract can also name a title field: the field that supplies the human label for its content in navigation, breadcrumbs and listings. The choice is inherited down the chain. Content with no title field falls back to the page’s own internal name, and the delivered titleSource member tells you which one you got (content or name).

Saving a Contract either updates its current version in place (an additive change, such as a new optional field) or creates a new version (a breaking change, such as a removed field or a new mandatory field). Existing content stays pinned to the version it was written against until it is upgraded. Every delivered descriptor names the version its content conforms to:

{ "id": "3f9c1e2a-…", "externalId": "article", "version": 4 }

A Component is one piece of content: a document that conforms to a Contract, such as a specific article. It has a Guid key, an optional external id, and a place in a folder tree. Folders organize the library and carry access rules, but they are not part of any URL.

A Component can be used in many places at once. One author Component can be referenced by every article that author wrote, and the same promotional banner can appear on several pages.

A structural field (Component, Presentation, and the embeds inside Rich Text) holds its content in one of two ways:

  • A reference points at a separate Component by provider and key. The target has its own life: it is edited, published and deleted on its own, and it can be referenced from anywhere. A reference can also carry contextualValues, which override the target’s Contextual fields for this one usage.
  • An inline value is content written directly inside its parent. It has a Contract but no key, and it is never published or fetched on its own. It is simply part of the parent’s document.

You can tell them apart in any response. A reference has provider and key; an inline value has neither:

{ "provider": "core", "key": "8b2a44f0-…", "contract": { "id": "", "externalId": "author", "version": 1 } }
{ "contract": { "id": "", "externalId": "author", "version": 1 }, "content": { "name": "Ada" } }

A reference comes with its content only when the response expanded it. Whether it did depends on the resolve depth you requested; see the resolvePath reference. An inline value always carries content, because there is nothing to fetch.

provider is core for content stored in ebitex Content. Other values name an external content provider the organization has connected. Their references deliver the same way.

A Template is a named way to show content, such as “Hero banner” or “Card grid”. It declares which Contracts it can present (often one abstract Contract), and it may declare settings: knobs that belong to the placement rather than to the content, such as a theme or a layout variant. Settings are themselves fields on an ordinary Contract, so they have types and modifiers like any other field.

A Presentation is one use of a Template: a Template, the content it shows, and that placement’s settings. Presentations are what pages are made of. A page’s body is usually an enumerable Presentation field, so the page is a list of presented sections, and the page itself is a Presentation too.

A delivered Presentation is an envelope:

{
"template": { "id": "", "externalId": "hero", "version": 2 },
"settings": { "theme": "dark" },
"component": { "provider": "core", "key": "", "contract": { "id": "", "externalId": "promo", "version": 4 } }
}

Your code picks a renderer by template.externalId and gives it settings and the component value. Keeping the Template separate from the content is what lets the same Component appear as a full hero on one page and a small card on another.

A Template expects content of a particular Contract. When the content an editor wants to show has a different Contract, an Adapter maps it: a set of rules that turns fields of one Contract into fields of another. The mapping runs on the server when the page is delivered, so your renderer receives content already shaped like the Contract it expects. A mapped binding carries an adapter member beside its contract, and its contract names the Adapter’s output Contract. There is no client-side mapping engine, so a mapped binding can only be rendered from a response that expanded it (a resolve depth of at least 1).

The Experience tree is where content gets a URL. Each site is a tree of nodes, and each node has a slug. A node’s path is its ancestors’ slugs joined together, and a site’s root node is always /. A site can have one or more hostnames mapped to it.

A node carries one of three kinds of payload:

  • A Presentation: the page shown at that path.
  • A Redirect: a pointer to another node, followed when a visitor requests the path.
  • Nothing: a structural node that only organizes the nodes beneath it and has no page of its own.

Nodes are separate from Components. A page node binds a Component through its Presentation, and the same Component can be bound at several paths. Deleting a page never deletes its content.

To render a URL, you ask Content what lives at it with resolvePath. To build navigation, you query the tree with listNodes, and getSitemap lists every published page.

This is a page resolved with resolvePath at a depth that expands its bound content. The page is a Presentation of a page Template. Its Component has a title, a list of sections (each one another Presentation), and a Rich Text body.

{
"kind": "presentation",
"nodeId": "0f6d…",
"path": "/coffees/ethiopia-guji",
"title": "Ethiopia Guji",
"titleSource": "content",
"presentation": {
"template": { "id": "", "externalId": "page", "version": 3 },
"settings": {},
"component": {
"provider": "core",
"key": "a41c…",
"contract": { "id": "", "externalId": "coffee-page", "version": 5 },
"contextualValues": {},
"content": {
"title": "Ethiopia Guji",
"sections": [
{
"template": { "id": "", "externalId": "hero", "version": 2 },
"settings": { "theme": "dark" },
"component": {
"provider": "core",
"key": "9e02…",
"contract": { "id": "", "externalId": "promo", "version": 4 },
"contextualValues": {},
"content": {
"headline": "Bright, floral, and sweet",
"image": {
"blobId": "c7d1…",
"contentType": "image/jpeg",
"sizeBytes": 184213,
"url": "https://…/c7d1….jpg"
},
"cta": {
"kind": "experience",
"target": { "provider": "core", "key": "" },
"url": "/shop",
"resolved": true
}
}
}
}
],
"body": [
{ "kind": "markdown", "markdown": "Grown at **2,000 metres** in the Guji zone." }
]
}
}
}
}

Some things to notice:

  • Every Presentation, at any depth, has the same template + settings + component shape, so one dispatch function renders the page and every section in it.
  • A Link field arrives with its url already composed for the site and locale you asked for. You never build a URL from a node id yourself.
  • A Rich Text field is an array. You iterate it and dispatch on kind (markdown, presentation, and so on) instead of scanning the markdown yourself.
  • A path that is a redirect answers { "kind": "redirect", "targetPath": "…", "targetNodeId": "…" } instead, and an unknown path answers 404 with path_not_found.

For typed access, the getSchemaBundle operation returns a JSON Schema describing the delivered shape of every published Contract, and @ebitex/content-sdk’s code generator turns it into TypeScript types.