Content Model
Content Model
ModulaCMS uses a schema-first content model. You define datatypes (content schemas) and fields (properties within those schemas), then create content data nodes that conform to those schemas. Content data nodes live in trees and store their values in content fields. A parallel admin content system provides CMS-internal configuration with the same structure.
Datatypes
A datatype is a content schema definition -- analogous to a "post type" in WordPress or a "content type" in Strapi. Datatypes define what kind of content a node represents and which fields it contains.
type Datatype struct {
DatatypeID DatatypeID `json:"datatype_id"`
ParentID *DatatypeID `json:"parent_id"`
Name string `json:"name"`
Label string `json:"label"`
Type string `json:"type"`
AuthorID *UserID `json:"author_id"`
DateCreated Timestamp `json:"date_created"`
DateModified Timestamp `json:"date_modified"`
}
| Field | Purpose |
|---|---|
DatatypeID |
ULID identifier, generated server-side |
ParentID |
Optional parent datatype for hierarchical organization |
Name |
Machine-readable identifier (e.g., hero_section) |
Label |
Human-readable display name (e.g., Hero Section) |
Type |
Categorization string that controls behavior |
Datatype Types
The Type field categorizes the datatype. Types prefixed with underscore (_) are engine-reserved and trigger built-in behavior. All other values are user-defined pass-through strings.
Reserved types (engine-enforced):
| Type | Purpose |
|---|---|
_root |
Tree entry point. Every route's content tree must have exactly one _root node. Its parent_id is always null. |
_reference |
Triggers tree composition. Resolves _id field values and attaches the referenced content trees as children. |
_nested_root |
Assigned at runtime during tree composition. When a _reference node's subtree is fetched, the fetcher replaces the subtree root's original type with _nested_root so the tree builder's root-finding logic (IsRootType) works recursively without modification. The _nested_root type persists in the delivered JSON output. |
_system_log |
Synthetic node injected when a reference cannot be resolved during composition. Contains error details. |
_collection |
Marks content as a queryable collection. Signals to clients that children support filtering and pagination via the query API. |
_global |
Singleton site-wide content (menus, footers, settings). Not tied to a route — delivered via the /globals endpoint. |
_plugin |
Plugin-provided content. Actual types use the _plugin_{name} namespace (e.g., _plugin_analytics), registered by the plugin system during initialization. |
User-defined types (pass-through, no engine behavior):
Any string not starting with _ is a valid user-defined type. Use descriptive strings to categorize your component datatypes:
| Example Type | Use Case |
|---|---|
section |
Layout sections (Hero, Footer, Sidebar) |
container |
Grouping containers (Cards, Tabs, Accordion) |
card |
Individual card components |
element |
Atomic UI elements |
Attempting to create a datatype with an unrecognized _-prefixed type returns an error.
Fields
A field defines a single property within a datatype schema. Fields specify the data type, validation rules, and UI rendering hints for content entry.
type Field struct {
FieldID FieldID `json:"field_id"`
ParentID *DatatypeID `json:"parent_id"`
SortOrder int64 `json:"sort_order"`
Name string `json:"name"`
Label string `json:"label"`
Data string `json:"data"`
Validation string `json:"validation"`
UIConfig string `json:"ui_config"`
Type FieldType `json:"type"`
Translatable int64 `json:"translatable"`
Roles []string `json:"roles"`
AuthorID *UserID `json:"author_id"`
DateCreated Timestamp `json:"date_created"`
DateModified Timestamp `json:"date_modified"`
}
| Field | Purpose |
|---|---|
ParentID |
The datatype this field belongs to |
SortOrder |
Display ordering within the datatype's field list |
Name |
Machine-readable name (e.g., featured_image) |
Label |
Human-readable name (e.g., Featured Image) |
Data |
JSON blob with type-specific configuration (e.g., select options, reference target) |
Validation |
JSON blob with validation rules (e.g., required, min/max length) |
UIConfig |
JSON blob with admin UI rendering hints (e.g., widget type, placeholder text) |
Type |
The field type -- determines storage, validation, and UI widget |
Translatable |
Non-zero if the field supports per-locale values for i18n |
Roles |
Restrict visibility to specific roles. nil means unrestricted. |
Field Types
The FieldType enum defines all available field types:
| Type | Description |
|---|---|
text |
Single-line plain text input |
textarea |
Multi-line plain text input |
number |
Numeric value (integer or decimal) |
date |
Calendar date without time (YYYY-MM-DD) |
datetime |
Date with time, serialized as RFC 3339 UTC |
boolean |
True/false value |
select |
Value from a predefined list of options (configured in the Data JSON blob) |
media |
Reference to a media asset (stores a MediaID) |
_id |
Content data ID (ULID). On _reference datatype nodes, the composition engine resolves this value to fetch and attach referenced subtrees at delivery time. |
json |
Arbitrary JSON data, preserved as-is without schema validation |
richtext |
Formatted text with markup (HTML or structured rich text) |
slug |
URL-safe slug, validated to contain only lowercase letters, numbers, and hyphens |
email |
Email address with format validation |
url |
URL string with format validation |
_title |
System title field (auto-generated for datatype display) |
plugin |
Plugin-provided field editor. The value is an opaque string produced by the plugin's coroutine-based interface. Configured via the field_plugin_config extension table. |
Custom field types can be registered via the FieldType API to extend the CMS.
Datatype-Field Junction
Fields are linked to datatypes through a junction table, allowing fields to be shared across multiple datatypes.
type DatatypeField struct {
ID DatatypeFieldID `json:"id"`
DatatypeID DatatypeID `json:"datatype_id"`
FieldID FieldID `json:"field_id"`
SortOrder int64 `json:"sort_order"`
DateCreated Timestamp `json:"date_created"`
DateModified Timestamp `json:"date_modified"`
}
SortOrder on the junction record controls the display ordering of fields within a specific datatype. This is separate from the field's own SortOrder, which is its default ordering.
Content Data
Content data represents the actual content -- a node in a content tree. Each content data node has a datatype (its schema), an optional route (its URL), and tree pointers for navigation.
type ContentData struct {
ContentDataID ContentID `json:"content_data_id"`
ParentID *ContentID `json:"parent_id"`
FirstChildID *string `json:"first_child_id"`
NextSiblingID *string `json:"next_sibling_id"`
PrevSiblingID *string `json:"prev_sibling_id"`
RouteID *RouteID `json:"route_id"`
DatatypeID *DatatypeID `json:"datatype_id"`
AuthorID *UserID `json:"author_id"`
Status ContentStatus `json:"status"`
PublishedAt *Timestamp `json:"published_at,omitempty"`
PublishedBy *UserID `json:"published_by,omitempty"`
PublishAt *Timestamp `json:"publish_at,omitempty"`
Revision int64 `json:"revision"`
DateCreated Timestamp `json:"date_created"`
DateModified Timestamp `json:"date_modified"`
}
The four tree pointers (ParentID, FirstChildID, NextSiblingID, PrevSiblingID) form a doubly-linked sibling list. See Tree Structure for details.
Status is either draft or published. See Publishing Lifecycle for the full workflow.
Revision is an incrementing counter that tracks how many times the content node has been modified.
Content Fields
Content fields store the actual values for a content data node. Each content field links to a field definition and a content data node.
type ContentField struct {
ContentFieldID ContentFieldID `json:"content_field_id"`
RouteID *RouteID `json:"route_id"`
ContentDataID *ContentID `json:"content_data_id"`
FieldID *FieldID `json:"field_id"`
FieldValue string `json:"field_value"`
Locale string `json:"locale"`
AuthorID *UserID `json:"author_id"`
DateCreated Timestamp `json:"date_created"`
DateModified Timestamp `json:"date_modified"`
}
All values are stored as strings in FieldValue, regardless of the field type. Numbers are stored as their string representation, booleans as "true" or "false", media references as the MediaID string, and JSON as a serialized JSON string.
Locale identifies the language/region for this field value. See Localization for how translatable fields work.
RouteID is denormalized on content field records for query performance. Always include the route ID when creating content fields.
Content Relations
Content relations represent directional references between content nodes through _id-type fields.
type ContentRelation struct {
ContentRelationID ContentRelationID `json:"content_relation_id"`
SourceContentID ContentID `json:"source_content_id"`
TargetContentID ContentID `json:"target_content_id"`
FieldID FieldID `json:"field_id"`
SortOrder int64 `json:"sort_order"`
DateCreated Timestamp `json:"date_created"`
}
Relations are created when a content node's _id-type field references another content node. SortOrder controls the display ordering when multiple relations exist on the same field.
At content delivery time, _id fields can compose the referenced node's subtree inline. The composition depth limit is configurable via composition_max_depth in modula.config.json (default: 10).
Admin Content System
ModulaCMS maintains a parallel admin content system for CMS-internal configuration. Admin content uses a separate set of tables with identical structure:
| User Content | Admin Content |
|---|---|
Datatype |
AdminDatatype |
Field |
AdminField |
DatatypeField |
AdminDatatypeField |
ContentData |
AdminContentData |
ContentField |
AdminContentField |
ContentRelation |
AdminContentRelation |
Route |
AdminRoute |
FieldTypeInfo |
AdminFieldTypeInfo |
ContentVersion |
AdminContentVersion |
The admin content system allows the CMS to manage its own internal content (dashboard configuration, system pages, navigation menus) using the same tree-based structure as user content, without namespace collisions with user-defined schemas.
Admin content is managed through separate API endpoints under /api/v1/admin/ and has its own publishing lifecycle.
Entity Relationships
Datatype (schema)
|
+--< DatatypeField >-- Field (property definition)
|
+--< ContentData (tree node)
|
+--< ContentField (value, keyed by FieldID + Locale)
|
+--< ContentRelation (references to other ContentData)
|
+-- Route (URL mapping)
A datatype defines the schema. Fields define the properties. DatatypeField links them together. Content data nodes use a datatype as their schema and store values in content fields. Content relations connect nodes through _id-type fields. Routes give content data nodes addressable URLs.
ID System
All entities use 26-character ULID identifiers wrapped in distinct Go types for compile-time safety. A ContentID cannot be passed where a UserID is expected. ULIDs encode a millisecond-precision timestamp, making them naturally sortable by creation time.
See the Glossary for a complete list of ID types.