TentoCMS
Api

Field Types

All 16 field types across 4 categories, with their options and use cases.

TentoCMS supports 16 field types organized into 4 categories. Each field type has specific options and use cases.

Field Type Categories

Text Fields

Suitable for textual content with various formatting options.

text

Single-line text input field.

Use cases:

  • Page titles
  • Article headlines
  • Names
  • Short descriptions
  • URLs

Options:

{
  placeholder?: string     // Placeholder text
  helpText?: string        // Helper message
  minLength?: number       // Minimum length (default: 0)
  maxLength?: number       // Maximum length (default: unlimited)
  pattern?: string         // Regex pattern the value must match
  patternMessage?: string  // Custom message shown when pattern validation fails
}

Example schema:

{
  "name": "title",
  "label": "Page Title",
  "type": "text",
  "required": true,
  "options": {
    "placeholder": "Enter page title",
    "maxLength": 200
  }
}

Editor experience:

  • Single line input
  • Character counter
  • Validation errors on save
  • Real-time length validation

textarea

Multi-line text input field.

Use cases:

  • Excerpts
  • Descriptions
  • Short biographies
  • Product summaries
  • Meta descriptions

Options:

{
  placeholder?: string     // Placeholder text
  helpText?: string        // Helper message
  minLength?: number       // Minimum length
  maxLength?: number       // Maximum length
  pattern?: string         // Regex pattern the value must match
  patternMessage?: string  // Custom message shown when pattern validation fails
}

Example schema:

{
  "name": "excerpt",
  "label": "Article Excerpt",
  "type": "textarea",
  "required": false,
  "options": {
    "placeholder": "Brief summary of the article",
    "maxLength": 500
  }
}

Editor experience:

  • Multi-line text box
  • Character counter
  • Resizable interface
  • Paste support

wysiwyg

Rich text editor with formatting capabilities (WYSIWYG - What You See Is What You Get). Powered by Tiptap editor.

Use cases:

  • Blog post content
  • Long-form articles
  • Page body content
  • Rich descriptions

Options:

{
  placeholder?: string // Placeholder text
  helpText?: string    // Helper message
}

Supported formats:

FormatDescriptionToolbar Button
BoldBold textB button
ItalicItalic textI button
UnderlineUnderlined textU button
StrikethroughStrikethrough textS button
Inline CodeInline code formatting</> button
HeadingsH1 through H6Dropdown menu
Bullet ListUnordered lists with nestingBullet icon
Numbered ListOrdered lists with nestingNumber icon
BlockquoteQuote blocksQuote icon
Code BlockMulti-line code blocksCode icon
Horizontal RuleDividing lineDivider icon
LinkHyperlinks with URL validationLink icon
ImageImages from media libraryImage icon
Text AlignLeft, center, right alignmentAlign icons
Undo/RedoHistory navigationArrow icons

Storage format: Content authored through the admin WYSIWYG editor is sanitized HTML by the time it's saved.

Allowed HTML tags:

p, h1, h2, h3, h4, h5, h6, blockquote, pre, code,
ul, ol, li, br, hr, strong, b, em, i, u, s, del,
a, span, img

Allowed attributes:

  • a: href, target, rel, title
  • img: src, alt, title, width, height
  • span, p, h1-h6, blockquote: class, style
  • pre, code, ul, ol, li: class

Security — client-side only, not enforced server-side:

  • HTML is sanitized via DOMPurify (isomorphic-dompurify), but this runs only in the admin editor's browser code (apps/admin/app/utils/html-sanitizer.ts) — it can't run during Cloudflare Workers SSR, so there's no server-side re-sanitization pass
  • XSS attacks are prevented through whitelist-based sanitization when content goes through the admin editor
  • External links get rel="noopener noreferrer" added automatically by the same client-side sanitizer
  • Only safe inline styles allowed, and data attributes are blocked — again, client-side only
  • Content written directly via the write API bypasses all of the above — the server does not re-run DOMPurify or any equivalent sanitization on wysiwyg field values, so API clients are responsible for sanitizing their own HTML before writing it

Example schema:

{
  "name": "content",
  "label": "Article Content",
  "type": "wysiwyg",
  "required": true,
  "options": {
    "placeholder": "Write your article..."
  }
}

Editor experience:

  • Full-featured toolbar with formatting controls
  • Link modal for URL insertion with validation
  • Media library integration for images
  • Keyboard shortcuts (Cmd/Ctrl+B for bold, etc.)
  • Real-time HTML sanitization
  • Prose-styled content preview
  • Dark mode support

Media Fields

For managing images and file uploads.

image

Image selection from media library.

Use cases:

  • Featured images
  • Hero section images
  • Product photos
  • Background images
  • Thumbnails

Options:

{
  helpText?: string          // Helper message
  allowedMimeTypes?: string[]// e.g., ["image/jpeg", "image/png"]
  maxFileSize?: number       // File size limit in bytes
}

Example schema:

{
  "name": "featuredImage",
  "label": "Featured Image",
  "type": "image",
  "required": false,
  "options": {
    "allowedMimeTypes": ["image/jpeg", "image/png", "image/webp"],
    "maxFileSize": 5242880
  }
}

Editor experience:

  • Media library browser
  • Upload new images
  • Crop/resize tools
  • Alt text editor
  • Image preview

Stored value shape:

The value saved in draftContent/publishedContent is normally a plain string — the media item's UUID:

{ "featuredImage": "550e8400-e29b-41d4-a716-446655440000" }

The admin UI's image field (apps/admin/app/components/fields/ImageField.vue) always writes this bare-string form on selection. Content produced by a CMS migration is instead written as a single-key object, { "_media": "<uuid>" } (see Migrations). The field component reads both shapes transparently — its extractMediaId() helper normalises { _media } back to a bare string as soon as the field loads and always emits a bare string on save — but the schema content-validation layer (validateMediaField() in packages/shared/src/schema/validator.ts) only recognises a bare string or an { id: "<uuid>" } object; it does not recognise { _media }. In practice this rarely surfaces because the editor normalises the shape on load, but a write-API client that saves an un-normalised { _media } value directly (bypassing the admin UI) could get an unexpected "must be a valid media reference" validation error.

Delivered shape differs by how the value was written — this is worth knowing before you build against it: the generic content-reference resolver (resolveReferences() in apps/api/src/utils/resolveReferences.ts, used by both the pages/collections public API and the admin preview endpoints) only recognises the { "_media": "<uuid>" } object shape — it resolves that into a full media object (_filename, _url, _mimeType, _width, _height, _altText). A bare-string value — the form the admin picker actually writes, and therefore the common case for content authored through the UI — is left completely unresolved and delivered as the raw UUID string, exactly as stored. Clients must call GET /api/v1/media/:id (see Media API (public)) themselves to turn that ID into a usable URL. This is unrelated to blog posts' dedicated featuredImage/featuredImageId pair, which is a hardcoded model field (not this schema-driven image type) always resolved to a full object by enrichBlogPostMedia — see Media Admin API for that shape.


media

Any file type from media library.

Use cases:

  • PDFs
  • Documents
  • Videos
  • Audio files
  • Downloadable assets
  • Mixed media collections

Options:

{
  helpText?: string          // Helper message
  allowedMimeTypes?: string[]// Allowed file types
  maxFileSize?: number       // File size limit in bytes
}

Example schema:

{
  "name": "downloadFile",
  "label": "Download",
  "type": "media",
  "required": false,
  "options": {
    "allowedMimeTypes": ["application/pdf", "application/msword"],
    "maxFileSize": 10485760
  }
}

Editor experience:

  • Media library browser
  • File type filtering
  • Upload interface
  • File preview
  • Metadata display

Stored value shape:

Same as image above — a plain string (the media item's UUID) in the common case:

{ "downloadFile": "550e8400-e29b-41d4-a716-446655440000" }

with the same { "_media": "<uuid>" } migration-content exception, the same admin field normalisation behaviour (apps/admin/app/components/fields/MediaField.vue), and the same content-validator caveat — see the image field's stored-value note above for the full detail.

See Media Admin API and Media API (public) for the full resolved Media entity shape.


Data Fields

For structured data entry and selection.

number

Numeric input field with optional constraints.

Use cases:

  • Prices
  • Quantities
  • Ratings/scores
  • Page order
  • Product inventory
  • Percentages

Options:

{
  placeholder?: string // Placeholder text
  helpText?: string    // Helper message
  min?: number         // Minimum value
  max?: number         // Maximum value
  step?: number        // Increment step (default: 1)
}

Example schema:

{
  "name": "price",
  "label": "Product Price",
  "type": "number",
  "required": true,
  "options": {
    "min": 0,
    "max": 10000,
    "step": 0.01
  }
}

Editor experience:

  • Number input with spinners
  • Min/max validation
  • Step controls
  • Number formatting

boolean

Toggle/switch field for true/false values.

Use cases:

  • Feature flags
  • Publish status
  • Show/hide sections
  • Yes/no questions
  • Enable/disable options

Options:

{
  helpText?: string // Helper message
}

Example schema:

{
  "name": "isFeatured",
  "label": "Featured Product",
  "type": "boolean",
  "required": false,
  "options": {
    "helpText": "Show this product in the featured section"
  }
}

Editor experience:

  • Toggle switch
  • Yes/no labels
  • Simple on/off interaction

date

Date picker field (YYYY-MM-DD format).

Use cases:

  • Event dates
  • Publication dates
  • Birth dates
  • Deadlines
  • Product launch dates
  • Availability dates

Options:

{
  helpText?: string // Helper message
}

Example schema:

{
  "name": "eventDate",
  "label": "Event Date",
  "type": "date",
  "required": true,
  "options": {
    "helpText": "When is this event happening?"
  }
}

Editor experience:

  • Calendar picker
  • Type-in date input
  • Keyboard navigation
  • Today button
  • Format validation

datetime

Date and time picker. The admin UI stores values in ISO 8601 format, but server-side validation (isValidDateTime() in packages/shared/src/schema/validator.ts) accepts any string new Date() can parse — it does not enforce strict ISO 8601 formatting. Content written directly via the API can therefore store non-ISO date strings.

Use cases:

  • Event start times
  • Scheduled publishing
  • Application deadlines
  • Meeting times
  • Timestamp recordings
  • Scheduled actions

Options:

{
  helpText?: string // Helper message
}

Example schema:

{
  "name": "publishedAt",
  "label": "Published Date & Time",
  "type": "datetime",
  "required": false,
  "options": {
    "helpText": "When should this content be published?"
  }
}

Editor experience:

  • Calendar date picker
  • Time input
  • Timezone support
  • ISO format display
  • Relative time

color

Color picker field.

Use cases:

  • Theme colors
  • Text colors
  • Background colors
  • Brand colors
  • Visual indicators
  • Design tokens

Options:

{
  helpText?: string // Helper message
}

Supported formats:

  • Hex colors: #FF0000, #F00
  • RGB: rgb(255, 0, 0)
  • RGBA: rgba(255, 0, 0, 0.5)
  • HSL: hsl(0, 100%, 50%)
  • HSLA: hsla(0, 100%, 50%, 0.5)
  • Named colors: red, blue, etc.

Example schema:

{
  "name": "brandColor",
  "label": "Brand Color",
  "type": "color",
  "required": true,
  "options": {
    "helpText": "Primary brand color"
  }
}

Editor experience:

  • Color picker widget
  • Format conversion
  • Copy hex/rgb/hsl
  • Color history
  • Transparency slider

json

Raw JSON editor for complex data structures.

Use cases:

  • Configuration data
  • Complex data structures
  • Custom properties
  • Settings objects
  • Metadata
  • Analytics data

Options:

{
  helpText?: string // Helper message
}

Example schema:

{
  "name": "metadata",
  "label": "Custom Metadata",
  "type": "json",
  "required": false,
  "options": {
    "helpText": "Custom JSON data for this page"
  }
}

Editor experience:

  • Syntax-highlighted JSON editor
  • Format validation
  • Pretty-print
  • Collapse/expand
  • JSON linting

select

Dropdown with a predefined set of options.

Use cases:

  • Status values
  • Category selection from a fixed list
  • Layout/variant choices
  • Sizes
  • Any single-choice value from a known set

Options:

{
  helpText?: string          // Helper message
  choices?: Array<{          // Predefined options
    label: string            // Display label
    value: string            // Stored value
  }>
}

Example schema:

{
  "name": "status",
  "label": "Status",
  "type": "select",
  "required": true,
  "options": {
    "choices": [
      { "label": "Draft", "value": "draft" },
      { "label": "Published", "value": "published" },
      { "label": "Archived", "value": "archived" }
    ]
  }
}

Validation:

  • The stored value must be a string.
  • If choices are defined, the value must match one of the defined value entries.

Editor experience:

  • Dropdown selector
  • Single choice
  • Predefined options only

Advanced Fields

For managing relationships and component composition.

reference

Link to items in a collection type.

Use cases:

  • Author references
  • Category assignments
  • Related items
  • Product variants
  • Department assignments
  • Team members

Options:

{
  helpText?: string       // Helper message
  collectionType?: string // Target collection slug — required only in the default/collectionItem
                           // reference mode (referenceType unset or 'collectionItem'); forbidden
                           // when referenceType is 'user'/'blogPost'; unused when 'page'
  multiple?: boolean      // Allow multiple selections
  referenceType?: 'page' | 'collectionItem' | 'user' | 'blogPost'  // Reference mode (default: 'collectionItem')
  pageType?: string       // Target page type slug — only applicable when referenceType is 'page'
}

referenceType selects the reference mode: the default/collectionItem mode links to a collection item (and requires collectionType); page links to a page (optionally scoped by pageType, collectionType unused); user/blogPost link to a project member or blog post respectively and must not set collectionType or pageType at all.

Example schema:

{
  "name": "author",
  "label": "Author",
  "type": "reference",
  "required": true,
  "options": {
    "collectionType": "authors",
    "helpText": "Select the article author"
  }
}

Multiple selections:

{
  "name": "relatedProducts",
  "label": "Related Products",
  "type": "reference",
  "required": false,
  "options": {
    "collectionType": "products",
    "multiple": true,
    "helpText": "Select related products to display"
  }
}

Editor experience:

  • Collection item browser
  • Search/filter
  • Multi-select (if enabled)
  • Item preview
  • Batch selection

Stored value shape:

For a single reference (multiple unset or false), the stored value is an object matching ReferenceValue (packages/shared/src/types/index.ts):

{ "author": { "_ref": "550e8400-e29b-41d4-a716-446655440000", "_type": "collectionItem" } }

_type is one of 'page' | 'collectionItem' | 'user' | 'blogPost' and always matches the field's referenceType option. For multiple: true, the stored value is an array of the same shape:

{
  "relatedProducts": [
    { "_ref": "550e8400-e29b-41d4-a716-446655440000", "_type": "collectionItem" },
    { "_ref": "660e8400-e29b-41d4-a716-446655440001", "_type": "collectionItem" }
  ]
}

A bare string ID (or an array of bare string IDs, for multiple) is also accepted for backward compatibility, by both the content validator (validateReferenceField() in packages/shared/src/schema/validator.ts) and the admin reference field (apps/admin/app/components/fields/ReferenceField.vue). The admin UI's picker, however, always writes the { _ref, _type } object form — the bare-string form only appears in older or externally-written content.

Delivered shape mirrors the same split described for image/media above: the generic content-reference resolver (resolveReferences()) only recognises the { _ref, _type } object shape and resolves it into the full referenced entity (collection item content, or minimal page/blog-post/user metadata, depending on _type) for the public and preview APIs. A bare-string reference ID is left unresolved and delivered exactly as stored — it will not be expanded into the referenced entity's data. If a referenced entity's own content includes image/media fields, those fields are subject to the same bare-string-vs-{_media} resolution split once more, one level down.


componentPicker

Select and arrange multiple component instances.

Use cases:

  • Page section builders
  • Feature lists
  • Gallery layouts
  • Content blocks
  • Widget composition
  • Modular pages

Options:

{
  helpText?: string           // Helper message — accepted by the schema, but see note below
  allowedComponents?: string[]// Component slugs allowed (optional — omit to allow any component)
  maxItems?: number           // Maximum number of items
}

helpText is not exposed in the admin UI for this field type. The schema accepts it, and the API will store it if set directly, but the schema builder's field editor hides the help-text input entirely for componentPicker (and nestedComponent) fields — editors can't set it through the UI.

Example schema:

{
  "name": "sections",
  "label": "Page Sections",
  "type": "componentPicker",
  "required": false,
  "options": {
    "allowedComponents": ["hero", "features", "testimonials", "cta"],
    "maxItems": 20
  }
}

Editor experience:

  • Component selector
  • Drag-to-reorder
  • Add/remove components
  • Component preview
  • Inline editing
  • Duplicate items

nestedComponent

Embed a single component instance inline.

Use cases:

  • Required page sections
  • Header/footer
  • Sidebar widgets
  • Fixed layout sections
  • Mandatory components

Options:

{
  helpText?: string      // Helper message — accepted by the schema, but hidden in the admin UI (see note below)
  componentSlug: string  // Target component slug (REQUIRED)
}

helpText is not exposed in the admin UI for this field type, same as componentPicker above — the schema builder's field editor hides the help-text input for both component field types.

Example schema:

{
  "name": "header",
  "label": "Page Header",
  "type": "nestedComponent",
  "required": true,
  "options": {
    "componentSlug": "header"
  }
}

Editor experience:

  • Fixed component
  • Inline field editing
  • No reordering
  • Required validation
  • Compact display

repeater

A repeatable group of sub-fields. Each entry is an object matching the defined sub-field schema.

Use cases:

  • Lists of key/value pairs
  • FAQ items (question + answer)
  • Feature lists
  • Repeated structured rows
  • Timelines or step lists

Options:

{
  helpText?: string        // Helper message
  fields: SchemaField[]    // Sub-field definitions (REQUIRED, at least one)
  minItems?: number        // Minimum number of entries
  maxItems?: number        // Maximum number of entries
}

Disallowed sub-field types:

To prevent deep nesting, a repeater may not contain the following sub-field types:

  • componentPicker
  • nestedComponent
  • repeater (no nested repeaters)

For complex nested structures, use componentPicker instead.

Example schema:

{
  "name": "faqs",
  "label": "FAQs",
  "type": "repeater",
  "required": false,
  "options": {
    "minItems": 1,
    "maxItems": 20,
    "fields": [
      {
        "name": "question",
        "label": "Question",
        "type": "text",
        "required": true
      },
      {
        "name": "answer",
        "label": "Answer",
        "type": "textarea",
        "required": true
      }
    ]
  }
}

Editor experience:

  • Add/remove entries
  • Drag-to-reorder
  • Inline editing of sub-fields
  • Min/max item enforcement

Field Options Summary

Common Options

The schema accepts placeholder/helpText on any field type, and most editor UIs surface both. The exception is componentPicker/nestedComponent: the admin schema builder hides both inputs for these two field types (see the notes in their sections above), even though the underlying schema doesn't forbid setting them:

{
  placeholder?: string  // Hint text before value entered
  helpText?: string     // Explanation shown below field
}

Text Fields

  • text, textarea:
    • minLength?: number - Minimum character count
    • maxLength?: number - Maximum character count
    • pattern?: string - Regex pattern the value must match
    • patternMessage?: string - Custom validation message shown when pattern fails

Numeric Fields

  • number:
    • min?: number - Minimum value
    • max?: number - Maximum value
    • step?: number - Increment step (default: 1)

Media Fields

  • image, media:
    • allowedMimeTypes?: string[] - Allowed file types
    • maxFileSize?: number - Maximum file size in bytes

Reference Fields

  • reference:
    • collectionType?: string - Target collection; required only in the default/collectionItem reference mode, forbidden for user/blogPost, unused for page
    • multiple?: boolean - Allow multiple selections
    • referenceType?: 'page' | 'collectionItem' | 'user' | 'blogPost' - Reference mode (default: collectionItem)
    • pageType?: string - Target page type; only applicable when referenceType is page

Component Fields

  • componentPicker:
    • allowedComponents?: string[] - Allowed component slugs (optional — omit to allow any component)
    • maxItems?: number - Maximum items
    • helpText?: string - Accepted by the schema, but hidden in the admin UI for this field type
  • nestedComponent:
    • componentSlug: string - Target component (required)
    • helpText?: string - Accepted by the schema, but hidden in the admin UI for this field type

Field Validation

Type-Specific Validation

Text fields (text, textarea):

  • Type check: must be string
  • Length validation: enforces minLength and maxLength

Numbers:

  • Type check: must be number
  • Range validation: enforces min and max

Dates:

  • Type check: must be a date string new Date() can parse
  • date fields expect YYYY-MM-DD in the admin UI, but validation just checks the string parses as a valid date
  • datetime fields are stored as ISO 8601 by the admin UI, but validation (isValidDateTime()) accepts any string new Date() can parse — it does not enforce ISO 8601 formatting specifically

Colors:

  • Format validation: hex, rgb, rgba, hsl, hsla
  • Named colors supported — a fixed subset of roughly 55 CSS color names (not the full CSS3 named-color list)

References:

  • Type check: string ID for single, array for multiple
  • Existence check: referenced item must exist

Components:

  • Slug validation: component must exist
  • Nesting limit: maximum 1 level deep
  • Circular reference check: prevents self-references

Required vs Optional Fields

  • required: true - Editor must provide a value before saving
  • required: false - Editor may leave field empty

Validation errors will be shown for:

  • Missing required fields
  • Invalid data types
  • Constraint violations (length, range, etc.)

Default Values

The defaultValue property can be set on any field:

{
  "name": "status",
  "label": "Status",
  "type": "text",
  "required": true,
  "defaultValue": "draft"
}

Default values are used when:

  • Creating new content items
  • Schema has default specified
  • Editors don't explicitly provide a value

Field Naming Conventions

Valid Field Names

The validation regex is ^[a-zA-Z][a-zA-Z0-9_]*$ (packages/shared/src/schemas/index.ts):

  • Must start with a letter — either case is accepted, a-zA-Z. Uppercase-starting names like Title pass validation.
  • Can contain letters, numbers, and underscores: a-zA-Z0-9_
  • Hyphens and spaces are not allowed
  • Can use camelCase: firstName, lastName
  • Can use underscores: first_name (discouraged)

Convention, not a rule: the admin UI's schema builder auto-generates a field's name from its label and always produces a lowercase-starting camelCase name (e.g. label "First Name" → firstName). If you set name directly via the API, a leading-uppercase name is accepted by validation but goes against this convention — prefer lowercase-starting camelCase for consistency with UI-created fields.

Valid Examples

  • title
  • firstName
  • productId
  • meta_data
  • Title (unconventional but technically valid — starts with an uppercase letter)

Invalid Examples

  • first-name (hyphens not allowed)
  • 1st (starts with a number)
  • first name (spaces not allowed)

Choosing Field Types

For Text Content

  • text - Short text (titles, headlines)
  • textarea - Medium text (descriptions, excerpts)
  • wysiwyg - Long formatted text (articles, blog posts)

For Numbers

  • number - Any numeric value with optional constraints
  • boolean - Yes/no, true/false decisions

For Dates/Times

  • date - Date only (YYYY-MM-DD)
  • datetime - Date and time (ISO 8601)

For Media

  • image - Images specifically
  • media - Any file type

For Selection/Linking

  • reference - Link to collection items
  • componentPicker - Multiple components
  • nestedComponent - Single required component

For Complex Data

  • json - Unstructured or complex data
  • color - Color values with picker

Migration Guide

Changing Field Types

When changing a field's type on an in-use schema:

  1. Get breaking change warning - API shows warnings
  2. Acknowledge impact - Understand data loss
  3. Plan migration - Map old values to new format
  4. Update content - Fix invalid content items

Non-breaking Changes

These can be made without affecting content:

  • Changing label
  • Changing placeholder
  • Changing helpText
  • Adding optional field
  • Making required field optional
  • Changing option values (except type restrictions)

Best Practices

  1. Use specific types - Choose the most specific type for your data
  2. Add constraints - Use min/max/length to prevent invalid data
  3. Provide help text - Guide editors with clear explanations
  4. Plan ahead - Think about field constraints before creating
  5. Test schemas - Create test content to validate fields work as expected
  6. Document references - Note which collections are referenced
  7. Use meaningful names - Field names should describe the data
  8. Group related fields - Order fields logically in schema

Copyright © 2026