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.
Contracts
Section titled “Contracts”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.
Field types
Section titled “Field types”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.
The five modifiers
Section titled “The five modifiers”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.
Inheritance and abstract Contracts
Section titled “Inheritance and abstract Contracts”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).
Versions
Section titled “Versions”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 }Components
Section titled “Components”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.
References and inline values
Section titled “References and inline values”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
providerandkey. 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 carrycontextualValues, 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.
Templates and Presentations
Section titled “Templates and Presentations”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.
Adapters
Section titled “Adapters”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
Section titled “The Experience tree”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.
What a delivered page looks like
Section titled “What a delivered page looks like”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+componentshape, so one dispatch function renders the page and every section in it. - A Link field arrives with its
urlalready 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 answers404withpath_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.
Next steps
Section titled “Next steps”- Environments and keys: where content is authored and served, and which credential to use.
- Publishing: what reaches the Delivery API, and when.
- Render content in a static site or a server-rendered site to see the model rendered.