Meridian handbook
Content modelling

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

KeyTypeDefaultWhat it does
keystringThe handle of a custom type: lowercase letters, digits and underscores, starting with a letter (walk, landing_page). Declare key: or extends:, never both.
extendsstringThe 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/.
idstringwritten by check --fixThe type’s stable id, typ_ and ten characters. Written by the tooling, never by hand. A file that extends a type writes no id.
labellocale map or stringThe name editors see. A map gives one value per locale; a plain string is the same in every locale.
plurallocale map or stringThe 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.
templatestring<type>/entry.htmlThe 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.
singletonbooleanfalseOne document of the type per site (Singletons). Custom types only.
localisablebooleantruefalse leaves the type’s pages out of the translation signal in content health. It does not stop a document being written in several locales.
fieldslistThe fields (Fields).
groupslistThe editor’s layout (Groups).
validationlistCross-field rules (Validation).
variantsmappingNamed field sets a document chooses between (Variants).
siteslist of site keysevery siteThe sites a shared type is on (Types on some sites).
revisionsmappingthe install’sRevision retention for this type (Revision retention).
structured_datamappingThe type’s schema.org entity and an optional FAQ mapping (SEO and structured data).
embedmappingWhich 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_editablemappingengine-dmo: what a partner may change at each membership tier (Partners and the portal).
iconstringAccepted and stored; the admin does not show it today.
searchablebooleanfalseAccepted and stored; it does not change what search indexes today.
uristring or locale mapAccepted 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 title with type: portable_text fails: core field "title" has type text and cannot be retyped to portable_text.
  • A core field’s parameters may only narrow. A max may fall and a min rise; a list such as options, to, kinds, styles, lists, accept, countries or geometries may lose entries but not gain them; multiple: true may 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 label and help, and may be marked index, unique, protected, recommended, site_overridable, machine_translate or summary_of.
  • A required core field cannot be made optional: restating it with required: false fails. Restating an optional core field with required: true has 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 slug fails with --after names "slug", which is not a field in schema/page.yaml. Leave --after out 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
KeyTypeWhat it does
keystringThe group’s handle.
idstringWritten by check --fix (grp_...).
labellocale map or stringThe group’s heading.
fieldslistThe fields it holds, by name. Each must be a field of the type or an engine-managed field such as tags or noindex.
collapsedbooleanStarts 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 }
  • singleton belongs on a type declared with key:. On extends: it fails: singleton is set on extends: event.
  • engine-dmo schema apply creates 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’s routes.yaml can make a singleton its home page with home: { 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 }
KeyTypeWhat it does
variants.defaultstringThe variant a new document starts in; the first option when left out.
variants.options[].keystringThe variant’s handle: lowercase letters, digits and underscores.
variants.options[].idstringWritten by check --fix (var_...).
variants.options[].labellocale map or stringThe name editors see.
variants.options[].templatestringReplaces the type’s template for documents in this variant.
variants.options[].fieldslistFields 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 read entry.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: in config/sites/<site>/schema/, and exists on that site only.
  • A shared type on some sites is declared once in config/schema/ with sites: [town, county]. Each listed site may extend it from its own schema directory with extends:, 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
KeyTypeWhat it does
prunebooleanfalse keeps every revision of the type.
keep_allduration such as 90dKeep every revision younger than this. At least a day.
keep_lastwhole number, at least 1And the newest N of each chain.
keep_monthlybooleanAnd the newest of each calendar month (UTC).

A key you leave out keeps the install’s value.

Errors you may see

MessageCause
<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 bothRemove 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 declaredTwo directories declare the same key, or a site declares a key the shared schema has.
type key "<key>" must be lowercase letters, digits and underscoresFix 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 itAdd 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 noneRemove 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.