Content › Fields
Docs/Content/Fields

Fields

What you'll learn

Every field type, its options, what it looks like in the admin, and how it is stored.

Before this page: Configuration.

Every field has a name and a type. Common options:

Option
labelString or { en, th }. Defaults to the humanized name.
requiredMust have a value (skipped while saving a draft).
defaultValueUsed when creating a document without this field.
uniqueNo two documents may share the value (top-level fields).
indexCreate a database index.
validate(value, { data, operation }) => true | 'error message', may be async.
access{ read, update } field-level access.
hiddenStored, but never returned by the API nor accepted as input.
localizedOne value per locale; see Localization.
position'sidebar': shown in the edit page's side panel (for top-level fields such as a category, tags or a date).
admin{ component, after }: admin components instead of the input, and below the field.

Types

TypeValueOptions
textstringminLength, maxLength
textareastringminLength, maxLength
emailstring, stored lowercase
numbernumbermin, max
booleanboolean
dateISO 8601 string (accepts Date)
selectone of the options, or an array with hasManyoptions, hasMany
slugURL-safe string, unique in the collectionfrom
jsonany JSON value
richTextTiptap JSON documentsee Rich text
uploadid of a media document, or an array with hasManyhasMany, mimeTypes: see Uploads
relationshipid(s) of documents in another collectionto, hasMany, minRows, maxRows
arraylist of rows, each with an id and sub-fieldsfields, minRows, maxRows
groupnested objectfields
blockslist of rows of different kinds, each with id and blockTypeblocks, minRows, maxRows

select

{ name: 'kind', type: 'select', options: ['news', { label: { en: 'Blog', th: 'บล็อก' }, value: 'blog' }] }
{ name: 'tags', type: 'select', options: ['vue', 'react'], hasMany: true }

slug

{ name: 'slug', type: 'slug', from: 'title' }

Filled from title when empty. Letters of any script are kept (สวัสดี ชาวโลก → สวัสดี-ชาวโลก); duplicates get -2, -3. Changing the title later does not change the slug.

uniqueWithin: 'parent' makes slugs unique only among documents with the same value of another field, e.g. pages under the same parent (as the nested pages plugin does).

relationship

{ name: 'author', type: 'relationship', to: 'users' }
{ name: 'related', type: 'relationship', to: 'posts', hasMany: true }

Ids are checked to exist when saving. Reads populate related documents to depth levels (default 1, max 3); at depth 0 you get ids. Deleted or unreadable documents become null, or are dropped from hasMany lists.

filterOptions limits which documents a relationship may point to. It runs on the server with the document's id (undefined while creating) and the user, and returns a where on the target collection, or true for any. The admin's picker offers only those, and saving checks them:

{
  name: 'manager',
  type: 'relationship',
  to: 'users',
  filterOptions: ({ id }) => ({ role: { equals: 'admin' } }),
}

array and group

{
  name: 'links',
  type: 'array',
  maxRows: 5,
  fields: [
    { name: 'label', type: 'text', required: true },
    { name: 'url', type: 'text' },
  ],
}
{ name: 'seo', type: 'group', fields: [{ name: 'title', type: 'text' }] }

Updating an array replaces all its rows; keep a row's id to keep its identity.

blocks

Rows of different kinds, for pages editors lay out themselves:

{
  name: 'layout',
  type: 'blocks',
  blocks: [
    {
      slug: 'hero',
      labels: { singular: 'Hero' },
      fields: [
        { name: 'heading', type: 'text', required: true },
        { name: 'image', type: 'upload' },
      ],
    },
    { slug: 'text', fields: [{ name: 'body', type: 'richText' }] },
  ],
}

Each row is { id, blockType, ...fields }; blockType picks the block. Rows are validated, relationships and uploads inside them populated, and the generated types are a union of the block kinds.

Blocks are stored as JSON. where can look inside them: a field name means that field in whichever block has it, and a document matches when some block does.

await cms.find('pages', { where: { 'layout.blockType': { equals: 'hero' } } })
await cms.find('pages', { where: { 'layout.heading': { like: 'sale' } } })
await cms.find('pages', { where: { 'layout.heading.en': { equals: 'Hello' } } }) // a locale
await cms.find('pages', { where: { layout: { exists: false } } }) // no blocks at all

Lists inside blocks work the same way: some row must match.

await cms.find('pages', { where: { 'layout.meta.tone': { equals: 'warm' } } }) // a group
await cms.find('pages', { where: { 'layout.items.title': { equals: 'One' } } }) // an array
await cms.find('pages', { where: { 'layout.tags': { in: ['sale'] } } }) // hasMany values
await cms.find('pages', { where: { 'layout.content.blockType': { equals: 'quote' } } }) // blocks

sort uses the first value found, going through the blocks (and the rows of lists in them) in order: sort: '-layout.columns', sort: 'layout.items.title'. These queries read the JSON of each document, so they suit filters more than large, hot lists.

How fields are stored

Each collection is a table; fields are columns (group fields are flattened: seo.title → seo_title). Arrays and hasMany values live in child tables; blocks are a JSON column. Two fields that would map to the same column are reported as a config error.

Next steps