Content types
Every key a type file takes (key, extends, label, ordering, template, singleton, variants, sites, groups, revisions and the rest), how to extend a core type, and the rules check enforces.
A type file declares one content type, or adds to one. This page is the reference for every key at the top level of a type file. Fields have their own page: Fields.
A custom type and an extension
A custom type is declared with key::
# config/schema/walk.yaml
key: walk
label: { en-GB: Walk, cy-GB: Taith gerdded }
plural: { en-GB: Walks, cy-GB: Teithiau cerdded }
ordering: { field: title, direction: asc }
fields:
- { name: title, type: text, required: true, index: true }
- { name: slug, type: slug, source: title, required: true } A core type is extended with extends:. The file adds fields and may restate core fields to relabel or narrow them:
# config/schema/article.yaml
extends: article
fields:
- name: summary # a core field: relabel, add help, narrow its parameters
max: 200
help: { en-GB: Shown on cards and in search results. }
- { name: heroImage, type: asset }
groups:
- key: main
label: { en-GB: Main }
fields: [title, slug, summary, body, heroImage] Type keys
| Key | Type | Default | What it does |
|---|---|---|---|
key | string | The handle of a custom type: lowercase letters, digits and underscores, starting with a letter (walk, landing_page). Declare key: or extends:, never both. | |
extends | string | The core type this file adds to (page, article, event, guide, collection, and in engine-dmo listing and area). From a site’s schema directory it may also name a shared custom type declared in config/schema/. | |
id | string | written by check --fix | The type’s stable id, typ_ and ten characters. Written by the tooling, never by hand. A file that extends a type writes no id. |
label | locale map or string | The name editors see. A map gives one value per locale; a plain string is the same in every locale. | |
plural | locale map or string | The plural name, used in the admin’s navigation and lists. | |
ordering | { field, direction } | The default order of the type’s lists and index pages. field is a field with index: true, or createdAt, updatedAt or publishedAt. direction is asc or desc; anything but desc sorts ascending. | |
template | string | <type>/entry.html | The template a document of the type renders with, relative to a template root. engine-dmo check fails when a routed type has no template on a site. |
singleton | boolean | false | One document of the type per site (Singletons). Custom types only. |
localisable | boolean | true | false leaves the type’s pages out of the translation signal in content health. It does not stop a document being written in several locales. |
fields | list | The fields (Fields). | |
groups | list | The editor’s layout (Groups). | |
validation | list | Cross-field rules (Validation). | |
variants | mapping | Named field sets a document chooses between (Variants). | |
sites | list of site keys | every site | The sites a shared type is on (Types on some sites). |
revisions | mapping | the install’s | Revision retention for this type (Revision retention). |
structured_data | mapping | The type’s schema.org entity and an optional FAQ mapping (SEO and structured data). | |
embed | mapping | Which fields the embeddable widgets show of a document of the type: image (an asset field), address (an address field), phone (a phone field) and website (a link or text field). A role left out takes the type’s first field of that type; website is shown only when named. Widgets are served when the site sets embed.enabled (Site configuration), and engine-dmo export embeds writes them for a static build (export). | |
member_editable | mapping | engine-dmo: what a partner may change at each membership tier (Partners and the portal). | |
icon | string | Accepted and stored; the admin does not show it today. | |
searchable | boolean | false | Accepted and stored; it does not change what search indexes today. |
uri | string or locale map | Accepted and stored; it does not give the type an address. Addresses come only from a site’s routes.yaml. |
Locale maps
Every label, plural, help text, block description, option label and validation message is either a plain string, used in every locale, or a map of locale code to text:
label: { en-GB: Opening hours, cy-GB: Oriau agor, ja-JP: 営業時間 } With a map, engine-dmo check reports each locale some site enables that has no value: a missing source locale is an error, any other locale a warning (the admin shows the source locale’s label until one is added). i18n.require_all: true in engine.yaml makes every enabled locale an error; false checks the source locale only.
Splitting a type across files
A type may be declared across several files in the same directory: config/schema/walk/fields.yaml with key: walk and config/schema/walk/rules.yaml with key: walk are one type. Fields, groups and validation rules from every file are combined. Every other key (label, plural, ordering, template, variants, embed, structured_data, revisions, id and so on) may be set in one file only; a second file setting it fails with ordering of type "walk" is already set in config/schema/walk/fields.yaml.
Extending a core type
extends: adds fields to a core type and lets you change how its core fields appear. The engine builds one type from the core definition, the shared extension and, on a site, that site’s own extension:
flowchart LR core["core listing<br/>title, slug, summary,<br/>location, primaryTag"] --> merged["listing on site town"] shared["config/schema/listing.yaml<br/>extends: listing<br/>adds phone, website"] --> merged site["config/sites/town/schema/listing.yaml<br/>extends: listing<br/>adds parkingNotes"] --> merged merged --> other["listing on any other site:<br/>core + shared fields only"]
The rules:
- A core field’s type is fixed. Restating
titlewithtype: portable_textfails:core field "title" has type text and cannot be retyped to portable_text. - A core field’s parameters may only narrow. A
maxmay fall and aminrise; a list such asoptions,to,kinds,styles,lists,accept,countriesorgeometriesmay lose entries but not gain them;multiple: truemay not be added. Anything else is refused with the reason:core field "summary": max 400 is larger than the core 280. - A core field may be given a new
labelandhelp, and may be markedindex,unique,protected,recommended,site_overridable,machine_translateorsummary_of. - A required core field cannot be made optional: restating it with
required: falsefails. Restating an optional core field withrequired: truehas no effect; use a validation rule to require it. - A restated core field writes no
id:; it keeps the engine’s. - An added field must not reuse a name the type already has, and must not use an engine-managed name (Reserved names).
engine-dmo dev field add <type> <name> --after <field>on an extended type can only name a field written in your extending file. The core fields are not in it, so--after slugfails with--after names "slug", which is not a field in schema/page.yaml. Leave--afterout and lay out the editor with groups.
Extending page
The core page type has only title and slug. Give it a summary (which head() prints as the meta description), a body and an SEO field before you write any page with content:
# config/schema/page.yaml
extends: page
fields:
- { name: summary, type: text, multiline: true, max: 280 }
- { name: body, type: portable_text, styles: [normal, h2, h3], lists: [bullet, number], marks: [strong, em, link] }
- { name: meta, type: seo } seo is a reserved name, so the SEO field is called something else (meta here). engine-dmo check --fix writes the three ids; engine-dmo describe type page then lists the core fields followed by these.
Reserved names
These names are kept by the engine on every type and cannot be declared as top-level fields: author, createdAt, id, locale, noindex, organisation, publishedAt (except as the article’s core field), redirectFrom, seo, status, tags, translations, type, updatedAt, uri, url. A type with variants also cannot have a field named variant.
A type routed with nesting on some site carries an implicit parent field, a single relation to its own type (Sites, locales and routes).
Groups
Groups lay out the editor. They never change storage, so regrouping is always safe.
groups:
- key: essentials
label: { en-GB: Essentials }
fields: [title, slug, summary, primaryTag, tags]
- key: contact
label: { en-GB: Contact }
fields: [address, phone, website]
- key: seo
label: { en-GB: Search }
fields: [meta, noindex]
collapsed: true | Key | Type | What it does |
|---|---|---|
key | string | The group’s handle. |
id | string | Written by check --fix (grp_...). |
label | locale map or string | The group’s heading. |
fields | list | The fields it holds, by name. Each must be a field of the type or an engine-managed field such as tags or noindex. |
collapsed | boolean | Starts the group closed. |
engine-dmo dev group add adds a group to a type file with its id.
Singletons
singleton: true on a custom type gives every site the type is on exactly one document of it: site settings, a home page’s own copy, a footer line.
# config/schema/site_settings.yaml
key: site_settings
label: { en-GB: Site settings }
singleton: true
fields:
- { name: title, type: text, required: true }
- { name: announcement, type: text, max: 160 } singletonbelongs on a type declared withkey:. Onextends:it fails:singleton is set on extends: event.engine-dmo schema applycreates each site’s singleton when it has none, as a draft in the site’s default locale, with the type’s label as its title and a slug from the title. Running apply again creates nothing, and an existing singleton is never touched. The plan names each one it will create.- Making an existing type a singleton is refused by the plan while any site holds more than one document of it. Turning it off leaves the documents as ordinary ones.
- A singleton cannot be trashed, cloned or shared to another site. The admin opens it straight in the editor instead of showing a list.
- Templates read it as
singletons.<type>, the current site’s published document in the request locale, or nothing (Globals). A site’sroutes.yamlcan make a singleton its home page withhome: { type: homepage }. - A seed document of a singleton type fills in that site’s singleton (Seeding and importing).
Variants
Variants are named field sets a document chooses between: an event that happens in a hall, one that is streamed, a festival with a line-up. They share the type’s own fields, routes, lists and permissions; each adds fields of its own.
# config/schema/event.yaml
extends: event
fields:
- { name: price, type: money, currency: GBP }
variants:
default: standard
options:
- { key: standard, label: { en-GB: Standard event } }
- key: online
label: { en-GB: Online event }
fields:
- { name: streamUrl, type: link, kinds: [{ of: url }], required: true }
- key: festival
label: { en-GB: Festival }
template: event/festival.html
fields:
- name: lineup
type: repeater
fields:
- { name: act, type: text, required: true } | Key | Type | What it does |
|---|---|---|
variants.default | string | The variant a new document starts in; the first option when left out. |
variants.options[].key | string | The variant’s handle: lowercase letters, digits and underscores. |
variants.options[].id | string | Written by check --fix (var_...). |
variants.options[].label | locale map or string | The name editors see. |
variants.options[].template | string | Replaces the type’s template for documents in this variant. |
variants.options[].fields | list | Fields this variant adds. use: is not allowed here. |
Rules engine-dmo check enforces:
- A variant cannot redeclare one of the type’s own fields; those are in every variant.
- A field two variants both want is declared identically in each, and is then one field whose value stays when a document moves between them. Declared differently, it fails.
- The type cannot have a field named
variant; templates readentry.variant.
The variant belongs to the document, not to a locale. Required fields and validation rules are the variant’s: a rule runs only in the variants that have every field it names. When an editor switches a document’s variant, values of fields the new variant lacks are set aside under _orphaned, not deleted, and switching back restores them. engine-dmo content variant set moves documents between variants in bulk (--dry-run says what each move would set aside). Seed documents name theirs with variant:.
Types on some sites
On an install with several sites, a type is on every site unless it says otherwise. Core types are on every site.
- A site’s own type is declared with
key:inconfig/sites/<site>/schema/, and exists on that site only. - A shared type on some sites is declared once in
config/schema/withsites: [town, county]. Each listed site may extend it from its own schema directory withextends:, adding fields offered on that site only.
# config/sites/town/schema/listing.yaml
extends: listing
fields:
- { name: parkingNotes, type: text, multiline: true } A type is refused wherever it is not on a site: the admin does not list it there, a route for it on another site fails engine-dmo check, and a document of it cannot be created, shared or cloned onto another site. A field added from a site’s directory is offered in that site’s editor only. member_editable (engine-dmo) may be set once in the shared file and once in each site’s directory, where it replaces the shared rule on that site; every other type key is set once per type.
engine-dmo dev type add <key> --sites town,county writes a shared type limited to those sites; --site town writes a site’s own type.
Revision retention
Every save makes a revision. The install prunes old ones by content.revisions in engine.yaml (by default it prunes, keeping every revision younger than 90 days, the newest 20 of each chain, and the newest of each calendar month). A type can override any of these:
revisions: { keep_all: 180d, keep_last: 50, keep_monthly: true } revisions: { prune: false } # keep every revision of this type | Key | Type | What it does |
|---|---|---|
prune | boolean | false keeps every revision of the type. |
keep_all | duration such as 90d | Keep every revision younger than this. At least a day. |
keep_last | whole number, at least 1 | And the newest N of each chain. |
keep_monthly | boolean | And the newest of each calendar month (UTC). |
A key you leave out keeps the install’s value.
Errors you may see
| Message | Cause |
|---|---|
<file> declares neither key: nor extends: | Every type file needs one of the two. |
a schema file declares either extends (a core type) or key (a custom type), not both | Remove one. |
"<name>" is not a core type; core types are ... | extends: names a type that is not core. Use key: for a new type. In engine-cms, listing and area are not core. |
type "<key>" is already declared | Two directories declare the same key, or a site declares a key the shared schema has. |
type key "<key>" must be lowercase letters, digits and underscores | Fix the key. |
<attr> of type "<key>" is already set in <file> | A type split across files sets the same attribute twice. |
ordering field "<name>" needs index: true, or the type cannot be sorted by it | Add index: true to the field. |
group "<key>" references unknown field "<name>" | A group lists a field the type does not have. |
type <key> has the id <id>; a file that extends it takes its id and writes none | Remove id: from the extending file. |
Every problem is reported at once, each with the file, line and column. See Error codes for the codes check prints.
Content modelling
How a content type is declared in YAML under config/schema, with a complete worked example from an empty file to a rendered page.
Fields
The keys every field takes (name, type, label, required, index, translate, visible_when and the rest), field groups, and a table of all 23 field types with links to each type's page.