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 | |
|---|---|
label | String or { en, th }. Defaults to the humanized name. |
required | Must have a value (skipped while saving a draft). |
defaultValue | Used when creating a document without this field. |
unique | No two documents may share the value (top-level fields). |
index | Create a database index. |
validate | (value, { data, operation }) => true | 'error message', may be async. |
access | { read, update } field-level access. |
hidden | Stored, but never returned by the API nor accepted as input. |
localized | One 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
| Type | Value | Options |
|---|---|---|
text | string | minLength, maxLength |
textarea | string | minLength, maxLength |
email | string, stored lowercase | |
number | number | min, max |
boolean | boolean | |
date | ISO 8601 string (accepts Date) | |
select | one of the options, or an array with hasMany | options, hasMany |
slug | URL-safe string, unique in the collection | from |
json | any JSON value | |
richText | Tiptap JSON document | see Rich text |
upload | id of a media document | see Uploads |
relationship | id(s) of documents in another collection | to, hasMany |
array | list of rows, each with an id and sub-fields | fields, minRows, maxRows |
group | nested object | fields |
blocks | list of rows of different kinds, each with id and blockType | blocks, 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 allLists 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' } } }) // blockssort 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
- Access control: field-level access.
- Rich text: show rich text on your pages.