Skip to content
Esc
navigateopen⌘Jpreview
On this page

Content model

Every field type a collection or map can carry, what it looks like to an editor, and the TypeScript your generated client hands your code.

Marketing builds the model in the UI; your generated client is typed from it. This page is the field vocabulary: what each type holds for an editor, and what it hands your code.

Fields

Field In the UI In the client
Text A single line string
Rich text The collaborative editor, on its own page string of markdown
Number A number input number
Checkbox A checkbox boolean
Date A date picker string, YYYY-MM-DD
Select One option from a list the editor maintains a union of the option names
Multi-select Several of those options an array of that union
Link A URL string
Person A member of the organization { id, name }
Asset An uploaded file ContentAsset
Reference One entry of another collection ContentReference
Multi-reference Several entries of another collection ContentReference[]

Every field is optional and nullable in the generated type: a field an editor has not filled in is absent, so your code decides what an empty one means.

Keys

A field’s key is its name in API form — Published date becomes published_date — and it is what your code reads: post.fields.published_date. Renaming a field gives it a new canonical key. The old key stays in the API and generated client as a deprecated alias, returning the same current value, so downstream code keeps working while engineering migrates. Key-changing renames appear under Developers → Actions; display-only changes do not.

Deleting a field

Deleting a field takes it out of the app straight away: editors stop seeing it, and nobody can fill it in again.

Your code is told rather than broken, twice over. The key stays in your generated types marked @deprecated, so your editor strikes it through where you read it and your build carries on. And entries go on returning the last value the field held, so the page reading it renders exactly as it did before. A deletion made in the UI reaches a running dev server within seconds, so both matter: engineering migrates on its own schedule instead of meeting a red build, or a blank page, that someone else caused.

A deleted key is never handed back. Creating a field with the same name again gives it a new key, so nothing inherits the values entries still hold.

Completing a field action

A deprecated key stays until someone completes its migration deliberately, from Developers → Actions in the app. Completing a rename removes only the old alias. Completing a deletion erases the field and everything written under it: values, rich-text bodies, revision history, and every remaining alias.

Completion is the one content change that can break a build, which is why nothing does it on the model’s behalf: it is a developer’s call, made once the code reading the key has moved on.

Assets

An asset field holds one uploaded file: an image, a PDF, a video. The client gets everything needed to render or link it without a second request:

const post = await stet.posts.get('hello-world');

if (post.fields.cover) {
  const { url, name, contentType, size } = post.fields.cover;
  // A whole URL, and public: it serves to your readers with no API key.
}

url is public delivery. It is unguessable, immutable, and cacheable forever, so it belongs directly in an img tag or a download link. Content assets are the only assets served this way; the files behind Stet’s own UI, like avatars, stay behind a session.

The API returns asset URLs relative to itself, since the same bytes serve whichever origin you reach them through, and the client joins them to the origin in your stet.config.ts. That is why the URL you get is ready to use on your own site, which is a different origin from Stet.

The same join reaches inside both representations of rich text: an asset an editor embedded or linked arrives as a whole URL too. HTML is rendered from the editor document and sanitised by Stet; markdown remains available for LLMs, feeds, diffs and custom renderers.

function PostBody() {
  const body = post.fields.body;
  if (body == null) {
    return null;
  }

  return <article dangerouslySetInnerHTML={{ __html: body.html }} />;
}

If you call the REST API directly rather than through the client, join the paths yourself with assetUrl and resolveAssetPaths from @stetcms/client.

Images can be resized and re-encoded on the way out by adding w, h, and format:

<img src={`${post.fields.cover.url}?w=800&format=webp`} />

References

A reference field points at entries of another collection, which is how you model relationships Stet does not hard-code: an author on a post, related posts, a category that is itself an entry with its own fields.

The value carries enough to render a link, plus the slug to fetch the whole entry when you need it:

const post = await stet.posts.get('hello-world');

// { id, slug, title } — the collection it points at is in the field's doc comment.
post.fields.author?.title;

// Fetch the rest through the target collection.
const author = post.fields.author ? await stet.authors.get(post.fields.author.slug) : null;

A multi-reference is the same shape as an array, in the order the editor set.

Nothing cascades. Deleting a referenced entry does not delete the entry pointing at it: a single reference reads as null afterwards, and a multi-reference simply stops listing it. Your pages keep rendering.

Building your own structures

Stet deliberately has no taxonomy type, no repeater, and no nested groups. The primitives above compose into all three, and the shape stays yours rather than ours:

  • Categories or tags with their own pages — a Categories collection, and a multi-reference from Posts. Each category is an entry, so it gets a slug, a description, an image, and analytics of its own. (A multi-select is the lighter option when a tag is only ever a label.)
  • A hierarchy — a reference from a collection to itself, holding the parent.
  • A repeating list — a collection for the rows, and a multi-reference from whatever owns them. The rows become addressable and reusable, which a nested repeater never is.

Versions

Every entry keeps its history. Saving a field, renaming, or writing a body adds a version; a run of edits by one person folds into one so history stays readable. Open History on an entry to see who changed it and when, and restore any version, which puts back its values, title, slug, and bodies. A restore is itself a version, so it is never a one-way door.

Each entry keeps its 50 most recent versions.

Model kits

Organization owners and admins can export the current content model as a portable JSON model kit from Organization → Model kit. A kit contains the collections, maps, fields, select options, and reference relationships, with their exact slugs and field keys.

Select a kit while creating an organization to start with that model. The new organization is independently owned and contains no copied entries. Kits never include members, API keys, webhooks, analytics, assets, audit history, or other organization settings.

The audit log

Organization admins get Audit log: every change to the content and the model, who made it, and which surface it came through — the app, the AI assistant, an import, or a restore.

Was this page helpful?