Field Types
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:
| Format | Description | Toolbar Button |
|---|---|---|
| Bold | Bold text | B button |
| Italic | Italic text | I button |
| Underline | Underlined text | U button |
| Strikethrough | Strikethrough text | S button |
| Inline Code | Inline code formatting | </> button |
| Headings | H1 through H6 | Dropdown menu |
| Bullet List | Unordered lists with nesting | Bullet icon |
| Numbered List | Ordered lists with nesting | Number icon |
| Blockquote | Quote blocks | Quote icon |
| Code Block | Multi-line code blocks | Code icon |
| Horizontal Rule | Dividing line | Divider icon |
| Link | Hyperlinks with URL validation | Link icon |
| Image | Images from media library | Image icon |
| Text Align | Left, center, right alignment | Align icons |
| Undo/Redo | History navigation | Arrow 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, titleimg: src, alt, title, width, heightspan,p,h1-h6,blockquote: class, stylepre,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
wysiwygfield 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
choicesare defined, the value must match one of the definedvalueentries.
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
}
helpTextis 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 forcomponentPicker(andnestedComponent) 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)
}
helpTextis not exposed in the admin UI for this field type, same ascomponentPickerabove — 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:
componentPickernestedComponentrepeater(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 countmaxLength?: number- Maximum character countpattern?: string- Regex pattern the value must matchpatternMessage?: string- Custom validation message shown whenpatternfails
Numeric Fields
- number:
min?: number- Minimum valuemax?: number- Maximum valuestep?: number- Increment step (default: 1)
Media Fields
- image, media:
allowedMimeTypes?: string[]- Allowed file typesmaxFileSize?: number- Maximum file size in bytes
Reference Fields
- reference:
collectionType?: string- Target collection; required only in the default/collectionItemreference mode, forbidden foruser/blogPost, unused forpagemultiple?: boolean- Allow multiple selectionsreferenceType?: 'page' | 'collectionItem' | 'user' | 'blogPost'- Reference mode (default:collectionItem)pageType?: string- Target page type; only applicable whenreferenceTypeispage
Component Fields
- componentPicker:
allowedComponents?: string[]- Allowed component slugs (optional — omit to allow any component)maxItems?: number- Maximum itemshelpText?: 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
minLengthandmaxLength
Numbers:
- Type check: must be number
- Range validation: enforces
minandmax
Dates:
- Type check: must be a date string
new Date()can parse datefields expectYYYY-MM-DDin the admin UI, but validation just checks the string parses as a valid datedatetimefields are stored as ISO 8601 by the admin UI, but validation (isValidDateTime()) accepts any stringnew 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 likeTitlepass 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
titlefirstNameproductIdmeta_dataTitle(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:
- Get breaking change warning - API shows warnings
- Acknowledge impact - Understand data loss
- Plan migration - Map old values to new format
- 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
- Use specific types - Choose the most specific type for your data
- Add constraints - Use min/max/length to prevent invalid data
- Provide help text - Guide editors with clear explanations
- Plan ahead - Think about field constraints before creating
- Test schemas - Create test content to validate fields work as expected
- Document references - Note which collections are referenced
- Use meaningful names - Field names should describe the data
- Group related fields - Order fields logically in schema
Related Documentation
- API Reference
- Validation Rules
- Schema Builder Guide
- Media Admin API — full
Mediaentity shape, upload/list/folder endpoints (relevant toimage/mediafields) - Media API (public) — serving URLs and image-transform parameters for turning an
image/mediafield's stored media ID into a usable URL

