Document Models
Document models let you define structured schemas for your content types. Instead of relying on ad-hoc frontmatter fields, you declare which fields a page type should have, what types they are, whether they’re required, and what constraints apply.
Quick Start
Create a models/ directory in your project root and add a YAML file for each content type:
my-site/
config.yaml
models/
blog.yaml
event.yaml
content/
...
Example: Blog Post Model
# models/blog.yaml description: Blog post with author and category fields: author: type: string required: true label: Author Name category: type: enum values: [tutorial, news, opinion, release] required: true featured_image: type: media reading_time: type: integer constraints: min: 1 defaults: category: news
Pages using the blog template will automatically be validated against this model.
Field Types
Document models support 15 field types:
| Type | Description | YAML Example |
|---|---|---|
string | Short text | "Hello World" |
text | Multi-line text | "Long description..." |
integer | Whole number | 42 |
number | Decimal number | 3.14 |
boolean | True/false | true |
date | ISO date | "2026-06-15" |
datetime | ISO datetime | "2026-06-15T10:30:00" |
enum | Fixed set of values | "published" |
url | URL (absolute or site-relative) | "https://example.com" |
email | Email address | "user@example.com" |
list | Array of typed items | [a, b, c] |
map | Key-value pairs with typed values | {key: value} |
reference | Path to another page | "/blog/my-post" |
media | Path to a media file | "hero.jpg" |
object | Nested fields | {width: 100, height: 200} |
Complex Types
Enum fields require a values list:
status: type: enum values: [draft, review, published, archived]
List fields require an items definition:
tags: type: list items: type: string
Map fields use items for the value type:
metadata: type: map items: type: string
Object fields define nested fields:
dimensions: type: object fields: width: type: number required: true height: type: number required: true
Constraints
Add validation constraints to fields:
fields: price: type: number constraints: min: 0 max: 99999 title: type: string constraints: min_length: 1 max_length: 200 pattern: "^[A-Z]" tags: type: list items: { type: string } constraints: min_items: 1 max_items: 10 event_date: type: date constraints: future_only: true
Available Constraints
| Constraint | Applies to | Description |
|---|---|---|
min | integer, number | Minimum numeric value |
max | integer, number | Maximum numeric value |
min_length | string, text | Minimum character count |
max_length | string, text | Maximum character count |
pattern | string, text | Regex pattern to match |
min_items | list | Minimum item count |
max_items | list | Maximum item count |
future_only | date, datetime | Date must be in the future |
past_only | date, datetime | Date must be in the past |
Model Resolution
Accent CMS matches pages to models using this priority:
- Explicit model field – Set
model: blogin your page’s frontmatter - Template name match – A page using
template: eventmatchesmodels/event.yaml - Directory model – A
_model.yamlfile in the content directory applies to all pages in that directory
Explicit Model
--- title: My Event model: event start_date: 2026-08-01 ---
Directory-Level Model
Place a _model.yaml file in a content directory to apply a model to all pages within it:
# content/events/_model.yaml model: event
All markdown pages under content/events/ will be validated against the event model.
Defaults
Models can provide default values for fields. These are applied when a field is absent from the page’s frontmatter.
Per-field defaults (set on individual fields):
fields: capacity: type: integer default: 100
Top-level defaults (applied to custom frontmatter):
defaults: status: published featured: false
Per-field defaults take precedence over top-level defaults for the same field name. Existing frontmatter values are never overwritten.
Model Inheritance
Models can extend a parent model using extends. The child inherits all parent fields and can add new fields or override parent definitions:
# models/default.yaml name: Default Page fields: subtitle: type: string required: false hero_image: type: media required: false # models/blog.yaml name: Blog Post extends: default fields: category: type: enum required: true values: [tech, design, business, personal] reading_level: type: enum default: intermediate values: [beginner, intermediate, advanced]
The blog model inherits subtitle and hero_image from default, plus adds its own fields.
Inheritance rules:
- Child inherits all parent fields, defaults, and validation rules
- Child can override parent field definitions (e.g., make an optional field required)
- Child can add new fields
- Child cannot remove parent fields
- Single inheritance only (no multiple parents)
- Circular inheritance is detected and rejected at load time
- Deep chains work: grandparent -> parent -> child
Sharing a Social / SEO meta Schema via extends
Social sharing tags (Open Graph, Twitter Cards, and any other platform tags)
ride an open meta object on every page. The object always works as a raw map
with no model bound – the share card still renders. Binding a model adds
validation: an over-long description or a typo’d og:type / twitter:card
value is caught at content load instead of surfacing only when a human inspects
the unfurl.
Because object schemas are inline (there is no named $ref), the way to reuse
one meta schema across content types is to put it on a base model and
extends that base. The default theme ships exactly such a base model:
# themes/default/models/base.yaml name: Base Document fields: description: type: string constraints: max_length: 200 # platforms silently truncate longer values meta: type: object label: Social / SEO tags fields: og:type: # colon keys are supported as-is type: enum values: [website, article, profile] default: website twitter:card: type: enum values: [summary, summary_large_image] default: summary og:description: type: string constraints: max_length: 200
A content model inherits the whole schema – the description cap and every
declared meta sub-field – with a one-line extends:
# themes/default/models/docs.yaml name: Documentation Page extends: base
Two things make this safe to apply broadly:
- The object is an open carrier. Only the sub-fields you declare are
validated. Undeclared keys (
fediverse:creator,fb:app_id, the next network) pass through untouched – adding a platform is a content edit, not a schema change. - No field is required and the model is not strict. A page that omits
descriptionormetavalidates cleanly; the schema only catches mistakes in metadata that is actually present.
To validate your own pages, bind a model that extends: base the same way you
bind any model (by template name, an _model.yaml directory file, or an
explicit model: field – see Model Resolution). See the
Social Sharing and SEO Metadata
guide for how the resolved tags are emitted.
Cross-Field Validation Rules
Single-field validation catches type errors and constraint violations, but many content contracts involve relationships between fields. Use rules to define cross-field validation expressions:
# models/event.yaml fields: start_date: { type: date, required: true } end_date: { type: date, required: true } capacity: { type: integer, default: 0 } registration_url: { type: url } is_virtual: { type: boolean, default: false } location: { type: string } rules: - name: end_after_start description: End date must be on or after start date expression: "end_date >= start_date" - name: capacity_with_registration description: If capacity > 0, registration_url should be set expression: "capacity == 0 or registration_url is defined" severity: warning - name: virtual_or_location expression: "is_virtual or location != ''" severity: warning
Rules use MiniJinja expression syntax. Each rule’s expression is evaluated against the page’s frontmatter fields. If the expression evaluates to false, the rule fails.
Rule properties:
| Property | Required | Description |
|---|---|---|
name | yes | Unique identifier for the rule |
description | no | Human-readable explanation |
expression | yes | MiniJinja expression to evaluate |
severity | no | error (default) or warning |
Available operators: ==, !=, >, >=, <, <=, and, or, not, is defined, is undefined
Rules are evaluated only after all referenced fields pass individual validation. If start_date has a type error, the end_date >= start_date rule is skipped to avoid confusing cascading errors.
Module Constraints
For modular pages (pages with _module subdirectories), models can restrict which modules are allowed:
# models/landing.yaml modules: allowed: - hero - features - pricing - testimonials - cta required: - hero max: 8
Validation behavior:
- A module not in
allowedproduces aModelDisallowedModuleissue - A module in
requiredthat is missing produces aModelMissingModuleissue - Exceeding
maxmodules produces aModelTooManyModulesissue - If no
modulesblock is defined, all modules are allowed (backward compatible)
Module Field Validation
Models can also validate frontmatter within individual modules using module_models:
module_models: hero: fields: heading: type: string required: true background: type: media required: false features: fields: columns: type: integer default: 3 constraints: min: 1 max: 4
Section Constraints
For pages with named sections (---section: name--- markers), models can restrict which section names are valid:
# models/wiki.yaml sections: allowed: - overview - details - references - changelog
A section not in the allowed list produces a ModelDisallowedSection issue. If no sections block is defined, all section names are allowed.
Strict Mode
Enable strict_fields: true to warn about frontmatter fields not declared in the model:
strict_fields: true fields: title_field: { type: string } category: { type: enum, values: [a, b] }
Standard frontmatter fields (title, template, date, author, tags, etc.) are always allowed even in strict mode.
Theme Models
Themes can provide models in their models/ directory. Project models override theme models with the same name:
themes/default/models/blog.yaml # Theme-provided model
models/blog.yaml # Project override (wins)
Template Access
The resolved model name is available in templates as page.model:
{% if page.model == "event" %}
<div class="event-badge">Event</div>
{% endif %}
Validation
Model violations appear in the debug panel (dev mode) and in accent validate output:
- ModelMissingRequired – A required field is not present
- ModelTypeMismatch – A field value doesn’t match the expected type
- ModelConstraintViolation – A value violates a constraint rule
- ModelUnknownField – An undeclared field is present (strict mode only)
- ModelRuleViolation – A cross-field rule expression evaluated to false
- ModelDisallowedModule – A module not in the allowed list
- ModelMissingModule – A required module is absent
- ModelTooManyModules – Page exceeds the maximum module count
- ModelDisallowedSection – A section name not in the allowed list
- ModelFilenamePatternMismatch – A filename or directory name does not match the model’s pattern
Filename Patterns
Models can declare naming conventions for both the directory name and the file name of content pages. Patterns are composed of typed segments that are validated against the actual filesystem names.
Defining Filename Patterns
# models/event.yaml filename: directory: pattern: "{order}.{date}-{slug}" description: Events are ordered, date-prefixed directories file: pattern: "event.md" description: All event pages use the event template filename
Both directory and file are optional. Models without a filename block impose no naming constraints.
Pattern Segment Types
Each {placeholder} in a pattern maps to a segment type:
| Segment | Syntax | Matches | Examples |
|---|---|---|---|
| literal | Raw text | Exact string | event, .md |
order | {order} or {order:3} | N-digit integer | 01, 003 |
slug | {slug} | Kebab-case lowercase ASCII | my-first-post |
date | {date} | ISO 8601 date (YYYY-MM-DD) | 2026-03-03 |
year | {year} | 4-digit year | 2026 |
month | {month} | 2-digit month (01-12) | 03 |
day | {day} | 2-digit day (01-31) | 15 |
number | {number} or {number:4} | Digits | 0001, 42 |
enum | {enum:a,b,c} | One of listed values | bug, feature |
field | {field:name} | Slugified frontmatter value | ab-1234 |
semver | {semver} | Semantic version | 1.2.3 |
lang | {lang} | ISO 639-1 language code | en, de |
any | {any} | Any non-separator string | Wildcard |
Cross-Validation with Frontmatter
Date and field segments can reference frontmatter fields. The filename value must match the frontmatter value:
filename: directory: pattern: "{order}.{date}-{slug}"
If the directory is 03.2026-06-15-rust-conf but the page’s date frontmatter is 2026-07-01, a mismatch warning is produced.
Multi-Level Directory Patterns
Use / in a directory pattern to validate multiple directory levels:
filename: directory: pattern: "{year}/{month}/{slug}"
This validates the 3 innermost directory levels above the page file.
Inheritance
Filename patterns are inherited from parent models. A child model can override the parent’s patterns:
# models/content.yaml filename: directory: pattern: "{order}.{slug}" # models/event.yaml extends: content filename: directory: pattern: "{order}.{date}-{slug}"
If a child defines only a directory pattern, the parent’s file pattern (if any) is still inherited, and vice versa.
Validation Mode
Control how strictly model violations are enforced with the content.validation.mode setting:
# config.yaml content: validation: mode: warn # "warn" (default), "strict", or "off"
| Mode | Behavior |
|---|---|
warn | Log validation issues but serve pages normally. This is the default. |
strict | Pages with model errors return HTTP 500. Builds halt on errors. |
off | Skip model validation entirely. Useful during migration periods. |
In strict mode, accent build will stop and report errors if any page fails validation. The dev server will return a 500 error page for invalid pages, making issues immediately visible during development.
Individual rules can override the global mode with severity: warning to produce warnings even in strict mode.
CLI Tooling
Inspecting Models
Use accent model to inspect loaded models without reading YAML files directly.
List all models:
$ accent model list Models loaded: blog (A blog article with author, date, and tags) event (A calendar event with date, location, and capacity) 2 model(s) loaded
Show a model’s full schema:
$ accent model show event Model: Event Description: A calendar event with date, location, and capacity Fields: title string required Title start_date date required Start Date end_date date required End Date location string required Location capacity integer optional Maximum Capacity (default: 0) category enum [...] required Event Category Rules: end_after_start End date must be on or after start date (error) Filename: directory: {order}.{date:start_date}-{slug} file: event.md
Both subcommands support --json for programmatic output and --config to specify a config file.
Scaffolding Content
Use accent new to create content pages from a model:
# Create an event with field overrides accent new event "Rust Conf 2026" \ --path content/events/ \ --field start_date=2026-06-15 \ --field end_date=2026-06-17 \ --field location="Portland, OR" \ --field category=conference # Created: content/events/01.2026-06-15-rust-conf-2026/event.md
The generated file includes all model fields with defaults:
--- title: Rust Conf 2026 template: event start_date: 2026-06-15 end_date: 2026-06-17 location: Portland, OR capacity: 0 category: conference speakers: [] price: 0.0 status: published tags: [] --- # Rust Conf 2026
Key behaviors:
- Directory and file names follow the model’s
filenamepattern when defined {order}auto-increments from existing siblings in the target directory{date}and{slug}are derived from field values and the title- Without a filename pattern, defaults to
{slug}/{model}.md --dry-runpreviews the output without creating files--no-defaultsskips optional fields without defaults- Refuses to overwrite existing files
Validate with Model Filters
Filter validation output to model-specific issues:
# Show only model validation issues accent validate --models # Validate only pages matching a specific model accent validate --model=event
Collection Model Filter
Filter content collections by model type in frontmatter:
# Only include pages matching the "event" model content: items: "@self.children" model: event order: by: custom.start_date dir: asc
Query by Model
Filter and display model information in query results:
# Filter pages by model accent query list --model=event # Include model name in output accent query list --show-model
Plugin-Provided Models
Plugins can ship document model definitions alongside their WASM modules and templates. This allows plugins to enforce content contracts without requiring users to manually create matching model files.
Static Model Files
Plugins can include a models/ directory in their distribution:
plugins/
ecommerce/
plugin.toml
plugin.wasm
models/
product.yaml
cart.yaml
templates/
product.html.jinja
Model YAML files in the plugin’s models/ directory are loaded automatically during startup.
Dynamic Model Registration
Plugins can also register models programmatically via the register_models WASM hook. This enables config-driven schemas (e.g., available currencies, custom fields based on plugin settings).
Enable the hook in plugin.toml:
[hooks]
on_register_models = true
The plugin’s register_models function receives the plugin configuration as JSON and returns a JSON array of model definitions.
Override Priority
Models are loaded in priority order (highest priority wins when names conflict):
- Plugin models (highest) – from
plugins/<name>/models/andregister_modelshook - Project models – from
models/directory - Theme models (lowest) – from
themes/<name>/models/
When a conflict occurs, the higher-priority source wins completely (no field-level merging across sources).
Model Source Tracking
The accent model list command shows where each model originates:
$ accent model list Models loaded: product [plugin:ecommerce] (E-commerce product) blog [project] (Blog post) default [theme:default] (Default page)
Error Handling
Invalid plugin models produce startup warnings but do not prevent the server from starting. This ensures a broken plugin model does not take down the entire site.
Configuration
The models directory defaults to ./models relative to your project root. Override it in config.yaml:
content: models_directory: ./schemas validation: mode: warn