TentoCMS
Guides

Schema Builder

Using the Schema Builder to create and manage page type and component schemas.

A comprehensive guide for developers using TentoCMS's Schema Builder to create and manage page type and component schemas.

Table of Contents


Quick Start

Accessing the Schema Builder

  1. Log in to the TentoCMS admin panel
  2. Navigate to SettingsPage Types or SettingsComponents
  3. Click + New Page Type or + New Component
  4. The Schema Builder opens with an empty schema

Basic Workflow

  1. Enter basic info
    • Name: Display name for editors
    • Slug: URL-safe identifier (auto-generated from name)
  2. Add fields
    • Click Add Field
    • Select field type from the grid
    • Configure field properties
  3. Save
    • Click Save
    • Address any validation errors
    • Schema is published immediately

Creating Page Types

Page types define the structure for all pages of that type. They're templates for content editors.

Step-by-Step: Create a Blog Post Page Type

1. Navigate to Page Types

Settings → Page Types → Create Page Type

2. Enter Basic Information

  • Name: Blog Post
  • Slug: blog-post (auto-generated)

The slug identifies your page type in the system. Use lowercase, hyphens only.

3. Build Your Schema

Add fields in order:

Field 1: Post Title

  • Type: text
  • Label: Post Title
  • Required: Yes
  • Options:
    • Placeholder: Enter the article title
    • Max Length: 200

Field 2: Author

  • Type: reference
  • Label: Author
  • Required: Yes
  • Options:
    • Collection Type: authors

Field 3: Publish Date

  • Type: datetime
  • Label: Publish Date
  • Required: No

Field 4: Featured Image

  • Type: image
  • Label: Featured Image
  • Required: No

Field 5: Post Content

  • Type: wysiwyg
  • Label: Post Content
  • Required: Yes

Field 6: Tags

  • Type: reference
  • Label: Tags
  • Required: No
  • Options:
    • Collection Type: tags
    • Multiple: Yes

4. Review and Save

Check your schema is complete:

  • All required fields have descriptive labels
  • Options are set appropriately
  • Fields are in logical order

Click Save to create the page type.


Creating Components

Components are reusable schema fragments that can be embedded in page types or composed together.

Step-by-Step: Create a Hero Component

1. Navigate to Components

Settings → Components → Create Component

2. Enter Basic Information

  • Name: Hero Section
  • Slug: hero

3. Build Your Schema

Field 1: Headline

  • Type: text
  • Label: Headline
  • Required: Yes
  • Options:
    • Max Length: 200
    • Placeholder: Main headline

Field 2: Subheading

  • Type: textarea
  • Label: Subheading
  • Required: No
  • Options:
    • Max Length: 500

Field 3: Background Image

  • Type: image
  • Label: Background Image
  • Required: Yes

Field 4: Call-to-Action Buttons

  • Type: componentPicker
  • Label: CTA Buttons
  • Required: No
  • Options:
    • Allowed Components: button, icon-button
    • Max Items: 2

4. Save

Click Save. The Hero component is now available for use in page types and other components.


Using Components

In Page Types

Embed components to create flexible page structures.

Using Nested Component (Single, Required)

Example: Every page must have a header.

Page Type: Landing Page
├── header (nestedComponent → "header" component)
├── hero (nestedComponent → "hero" component)
└── sections (componentPicker → multiple components)

Schema field:

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

Using Component Picker (Multiple, Optional)

Example: Let editors build pages from sections.

Page Type: Landing Page
├── sections
    ├─ hero component
    ├─ features component
    ├─ testimonials component
    └─ [add more...]

Schema field:

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

In Other Components

Components can reference other components for modular design.

Example: Hero component allows button components.

Hero Component
├── title (text)
├── background (image)
└── buttons (componentPicker → button components)

Editors can customize the hero's buttons without creating new components.


Field Configuration

Adding Required vs Optional Fields

Required fields (required: true)

  • Enforced at publish time, not on save — editors can save (and keep saving) a draft with required fields left empty; the API only blocks the action when they try to publish
  • Use for content that must exist before a page or item goes live

Optional fields (required: false)

  • Can be left empty
  • Use for supplementary content
  • Provide defaults when possible

Naming Fields

Good field names:

  • Use camelCase: firstName, productId
  • Be descriptive: seoTitle (not just title)
  • One per line in the UI
  • Avoid abbreviations

Field names are auto-generated from labels:

Label: "Featured Image" → name: "featuredImage"
Label: "Meta Description" → name: "metaDescription"

Edit the name manually if you need different format.

Using Field Options

Every field type has specific options:

Text fields - Set character limits:

{
  "maxLength": 200,
  "minLength": 10
}

Number fields - Set numeric ranges:

{
  "min": 0,
  "max": 100,
  "step": 5
}

Reference fields - Link to other content. referenceType picks what the field links to (defaults to collectionItem); collectionType/pageType are only relevant for their matching mode:

// Default mode — links to a collection item
{
  "referenceType": "collectionItem", // or omit — this is the default
  "collectionType": "authors",
  "multiple": true
}

There are three other referenceType modes, each with its own picker in the field editor's "Reference Target" dropdown:

referenceTypeLinks toExtra option
collectionItem (default)An item in a collection typecollectionType (required)
pageA pagepageType (optional filter — leave empty to allow any page type)
blogPostA blog post
userA user

page/blogPost/user modes must not set collectionType; blogPost/user modes must not set pageType either — the field editor hides irrelevant options automatically when you switch referenceType.

Component fields - Limit allowed components:

{
  "allowedComponents": ["button", "icon-button"],
  "maxItems": 3
}

Repeater fields - A simpler alternative to componentPicker for repeatable groups of plain fields (text, number, image, etc. — not other components). Define the sub-fields once and editors add/reorder/remove items:

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

Reach for repeater when each item is just a handful of simple fields (an FAQ list, a set of stats); reach for componentPicker when items need to vary in type or reuse an existing component definition. Sub-fields inside a repeater can't themselves be componentPicker, nestedComponent, or another repeater — keep nested structures to componentPicker/nestedComponent instead.


Validation & Error Handling

Common Validation Errors

Duplicate Field Name

Error: Duplicate field name: title

Problem: Two fields have the same name

Fix: Rename one field to be unique

❌ title, title
✅ title, postTitle

Invalid Length Range

Error: minLength cannot be greater than maxLength

Problem: minLength is set higher than maxLength

Fix: Correct the values

❌ minLength: 200, maxLength: 100
✅ minLength: 10, maxLength: 200

Invalid Component Reference

Error: Component "nonexistent" does not exist

Problem: Referencing a component that doesn't exist

Fix:

  1. Check the component slug is correct
  2. Create the component first, then reference it
  3. Refresh to see newly created components

Circular Reference

Error: A component cannot reference itself

Problem: Component A references Component A (directly or indirectly)

Fix: Remove the self-reference

❌ Component A → nestedComponent → Component A
✅ Component A → nestedComponent → Component B

Nesting Limit Exceeded (not currently reachable)

Error: Component nesting is limited to 1 level

The validator defines this check, but no current save path (POST/PUT on page types or components) ever triggers it — every caller always validates at a fixed nesting level of 0, and that level is never incremented when validation recurses into a nested component's own schema. In practice, you can nest nestedComponent chains deeper than 1 level today (e.g. Component A → nestedComponent B → nestedComponent C) without hitting this error — only genuine circular references (a component pointing back to itself, directly or through a chain) are actually rejected. Deep, non-circular nesting is still worth avoiding for clarity and editor usability, but it is not currently blocked by validation.

Viewing Validation Errors

Errors appear in two places:

  1. Above the problematic field - Inline error messages
  2. At the top of the form - Summary of all errors

Address errors in order and save again.


Breaking Changes

When modifying a page type or component that has content, the API warns about breaking changes.

Breaking Change Types

FIELD_REMOVED

What: You deleted a field that exists in live content

Impact: Pages lose access to this data

Example:

Old schema:  [title, content, author]
New schema: [title, content]
Breaking change: author field removed

Decision:

  • Safe if the field wasn't widely used
  • Creates orphaned data (still in database but hidden)
  • Can be re-enabled by adding the field back

FIELD_TYPE_CHANGED

What: You changed a field's type

Impact: Existing content might become invalid

Example:

Old: author (reference)
New: author (text)
Breaking change: Type changed from reference to text

Decision:

  • Data loses its structure
  • Old IDs become text strings
  • Might need manual migration

FIELD_NOW_REQUIRED

What: You made an optional field required

Impact: Existing pages without this field are now invalid

Example:

Old: seoTitle (optional)
New: seoTitle (required)
Breaking change: Field is now required

Decision:

  • Existing pages must be edited to add values
  • New pages will require the field
  • Can create content management burden

Handling Breaking Changes

When you see warnings:

  1. Read the warning - Understand what changed
  2. Assess impact - How many pages are affected?
  3. Plan migration - Do you need to update pages?
  4. Decide to proceed - Is the change necessary?

The API still saves if you proceed. The warning is advisory.


Schema Organization

Ordering Fields Logically

Arrange fields in the order editors will fill them:

Blog Post:
1. title (essential, at top)
2. author (key metadata)
3. publishedAt (scheduling)
4. featuredImage (visual)
5. content (main work)
6. tags (categorization)
7. seoTitle (technical)
8. seoDescription (technical)

Group by purpose:

  • Essential fields first
  • Metadata middle
  • Technical/SEO last

Naming Conventions

Consistent naming:

✅ postTitle, postContent, postAuthor
❌ title, content, author_name

Prefixes for clarity:

seoTitle, seoDescription      (SEO fields)
metaAuthor, metaKeywords      (Metadata fields)
internalNotes, internalStatus (Admin-only fields)

Schema Size

Keep schemas focused:

  • 5-15 fields per page type
  • 3-8 fields per component
  • Too many fields overwhelms editors

Consider splitting:

❌ ProductPage: 25 fields
✅ ProductPage: core + section components for extensibility

Common Patterns

Blog Site

Page Types:

  • blog-post - Individual articles
  • blog-index - Blog listing page
  • blog-category - Category page

Components:

  • post-card - Blog post preview
  • author-bio - Author information
  • related-posts - Related articles list

E-Commerce Product

Page Types:

  • product - Product detail page

Components:

  • product-header - Title, price, rating
  • product-images - Image gallery
  • product-description - Details and specs
  • product-reviews - Customer reviews
  • related-products - Cross-sell items
  • add-to-cart-button - Purchase CTA

Landing Page

Page Types:

  • landing-page - Marketing page

Components:

  • hero - Hero section
  • features - Feature grid
  • testimonials - Customer quotes
  • pricing-table - Pricing options
  • cta-section - Call-to-action
  • faq - Frequently asked questions

Troubleshooting

Component Not Showing in Dropdown

Problem: Created a component but can't select it

Solution:

  1. Ensure component is saved (no errors)
  2. Refresh the page
  3. Check component slug matches exactly
  4. Make sure it's not soft-deleted

Can't Add Field Due to "Circular Reference"

Problem: Error when adding component reference

Solution:

  1. Check if component references itself
  2. Check for indirect chains (A→B→A)
  3. Try adding to a different component first
  4. Verify component hierarchy is acyclic

Schema Won't Save

Problem: Getting validation errors on save

Solution:

  1. Read the error message carefully
  2. Check for duplicate field names
  3. Verify component/collection references exist
  4. Ensure constraint ranges are valid (min ≤ max)
  5. Check field types are valid

Page Type Can't Be Deleted

Problem: "Cannot delete: X page(s) are using this type"

Solution:

  • Delete all pages of this type first
  • Archive pages instead of deleting
  • Create a new page type and migrate pages to it
  • Bulk update pages to different type (if needed)

Component Can't Be Deleted

Problem: Cannot delete component: it is used by N page(s)/item(s) (IN_USE, 409)

This is a content-embedding check, not a schema-reference check. Deletion is only blocked when the component is actually embedded in real page or collection-item content (its usageCount — see Usage Tracking — is greater than zero). A component that is merely listed in another page type's or component's schema (e.g. in a componentPicker's allowedComponents, or as a nestedComponent's componentSlug) but never actually used in any saved content is freely deletable — removing it from those schema references first is not required and won't change whether it can be deleted.

Solution:

  • Find and update (or delete) the pages/collection items that actually embed this component
  • Check the component's usageCount in the Components list to see how many records are affected
  • Once usageCount reaches 0, the component can be deleted regardless of any schema still listing it as an allowed/nested option

Advanced Topics

Using Placeholders Effectively

Placeholders guide editors on what to enter:

{
  "name": "metaDescription",
  "label": "Meta Description",
  "type": "text",
  "options": {
    "placeholder": "2-3 sentences describing the page (max 160 chars)"
  }
}

Good placeholders:

  • Show expected format
  • Mention character limits
  • Give examples
  • Suggest SEO best practices

Help Text for Complex Fields

Use help text to explain non-obvious fields:

{
  "name": "relatedProducts",
  "label": "Related Products",
  "type": "reference",
  "options": {
    "collectionType": "products",
    "multiple": true,
    "helpText": "Select up to 5 related products. These will appear in the 'You might also like' section at the bottom of the page."
  }
}

helpText supports a limited, safe subset of Markdown: **bold**/__bold__, *italic*/_italic_, `code`, and [link text](url) (http/https links only — other protocols are stripped). It's rendered to sanitised HTML wherever the field is shown to editors, so you can use it to add emphasis or link out to fuller documentation. Anything outside that subset (headings, lists, images, etc.) is not supported and renders as plain text.

Creating Versioned Schemas

For major changes, create a new page type:

blog-post (original)
blog-post-v2 (with new fields)
blog-post-v3 (refined)

Migrate pages gradually:

  1. Create new page type
  2. Create pages with new type
  3. Migrate old pages over time
  4. Eventually remove old type

This avoids breaking changes and allows gradual migration.


Best Practices Summary

  1. Plan first - Sketch your schema before creating
  2. Start simple - Add complexity gradually
  3. Use descriptive labels - Help editors understand fields
  4. Set constraints early - minLength, maxLength prevent issues
  5. Group components - Related fields together
  6. Document via help text - Explain non-obvious fields
  7. Test with real data - Create test content to validate
  8. Review breaking changes - Understand impact before saving
  9. Keep schemas focused - Don't include everything in one type
  10. Use components for reuse - Don't duplicate field definitions


FAQ

Can I rename a field after creating it?

Answer: Not in the same builder session — but the lock has nothing to do with whether any page or item actually has content yet. The Field Name input is disabled as soon as a field has been added to the schema in the builder (i.e. for any field you're editing rather than adding fresh); this is pure UI/session state in the Schema Builder, not a check against existing content. In other words, a brand-new page type with zero pages still locks its field names the moment you add them. If you need a different field name, create a new field with the name you want and migrate data across (data is keyed by name, so a straight rename would orphan any content already stored under the old key).

How many fields can I have?

Answer: No hard limit, but keep under 20 for usability. Use components to organize large schemas.

Can I add fields without updating existing pages?

Answer: Yes. New optional fields don't affect existing content. Existing pages will see the new field as empty.

What happens if I delete a page type?

Answer: Page types are soft-deleted. Existing pages orphaned. Can be restored from database if needed.

Can components reference other components?

Answer: Yes, using nestedComponent (single instance) or componentPicker (multiple). A component can't reference itself, directly or through a chain of other components (that's rejected as a circular reference) — but nesting depth beyond 1 level isn't currently enforced; see Nesting Limit Exceeded above.

What's the difference between nestedComponent and componentPicker?

Answer:

  • nestedComponent - Single fixed component instance
  • componentPicker - Zero or more components, editor-selected and reorderable

required is an independent, settable option on both field types — the same as on any other field — not something nestedComponent always carries. Set required: true on a nestedComponent when a page must always have that section (e.g. every page needs a header); leave it false (the default) when the section is optional. Use nestedComponent for a single fixed slot, componentPicker for a flexible, editor-managed list.


Support & Feedback

For questions or issues:

Copyright © 2026