Blocks and rich text
Repeaters (a list of items with the same fields), blocks (a flexible page body of typed blocks, each with its own template), shared blocks, and Portable Text rich text with styles, lists, marks, link annotations and object blocks, and how each renders.
Three field types hold structured, ordered content:
| Type | Use it for | Each item is |
|---|---|---|
repeater | A list of things of one shape: a walk’s stages, a venue’s rooms, questions and answers. | The same child fields. |
blocks | A flexible page body an editor composes and reorders: text, a quote, a gallery, a grid of cards. | One of the declared blocks, each with its own fields and its own template. |
portable_text | Formatted prose: headings, paragraphs, lists, links, bold and italic, with optional embedded objects. | A Portable Text block. |
repeater
fields:
- name: stages
type: repeater
min: 1
max: 20
collapsed: true
item_label: "{{ title }}"
fields:
- { name: title, type: text, required: true }
- { name: distanceKm, type: number, min: 0, index: true }
- { name: notes, type: portable_text, marks: [strong, em, link] } | Parameter | Type | Default | What it does |
|---|---|---|---|
fields | list | required | The child fields of every item, declared like any field. A child may itself be a repeater or blocks field. |
min | whole number | none | The fewest items. Fewer than min but more than none is an error on save; none is an error at publish. |
max | whole number | none | The most items. |
collapsed | boolean | false | Starts every item closed in the editor, so a long list reads as an outline. |
item_label | template | the first text-like value | The summary line of a collapsed item, such as "{{ title }}". A rich text child gives its plain text. |
A repeater must declare at least one child field (a repeater declares its item fields).
blocks
fields:
- name: body
type: blocks
collapsed: true
blocks:
- key: richText
label: { en-GB: Text }
description: { en-GB: A passage of formatted text }
item_label: "{{ content }}"
fields:
- name: content
type: portable_text
styles: [normal, h2, h3]
lists: [bullet, number]
marks: [strong, em, link]
- key: faq
label: { en-GB: Questions and answers }
fields:
- name: items
type: repeater
item_label: "{{ question }}"
fields:
- { name: question, type: text, required: true }
- { name: answer, type: portable_text, styles: [normal], marks: [strong, em, link] }
- use: quote | Parameter | Type | Default | What it does |
|---|---|---|---|
blocks | list | required | The block kinds an editor may add, in the order the block picker shows them. At least one. |
min, max | whole number | none | The fewest and most blocks, as for a repeater. |
collapsed | boolean | false | Starts every block closed; a block just added opens regardless. |
item_label | template | The collapsed summary when a block has none of its own. |
Each block declares:
| Key | Type | What it does |
|---|---|---|
key | camelCase string | The block’s handle, and the name of its template. Unique within the field. |
id | string | Written by check --fix (blk_...). |
label | locale map or string | Its name in the block picker. |
description | locale map or string | One line under the label in the block picker. |
item_label | template | Its collapsed summary, such as "{{ heading }}". |
fields | list | Its fields. |
use | shared block key | Instead of all the above: include a shared block. |
Shared blocks
A block several blocks fields offer is declared once in config/blocks/_index.yaml, with key:, and included with - use: <key>:
# config/blocks/_index.yaml
blocks:
- key: quote
label: { en-GB: Quote }
description: { en-GB: A pull quote with its source }
item_label: "{{ text }}"
fields:
- { name: text, type: text, multiline: true, required: true }
- { name: cite, type: text } A use: entry declares nothing else. A shared block is declared with key:, never use:, and once only.
Block templates
Each block renders through blocks/<key>.html in the site’s template roots (for example templates/_shared/blocks/quote.html). The page template renders the whole field with the blocks tag:
<div class="blocks">{% blocks entry.body %}</div> Inside a block template, the block’s fields are on block, with blockIndex (from zero) and blockCount, beside every global of the page:
{# templates/_shared/blocks/quote.html #}
<blockquote class="quote">
<p>{{ block.text|linebreaksbr }}</p>
{% if block.cite %}<footer>{{ block.cite }}</footer>{% endif %}
</blockquote> {# templates/_shared/blocks/faq.html #}
<section class="faq">
{% for item in block.items %}
<details>
<summary>{{ item.question }}</summary>
{{ item.answer|ptext }}
</details>
{% endfor %}
</section> engine-dmo check fails when a declared block has no template on a site: block "quote" of page.body has no template for site main, with the path to add. engine-dmo new block <key> writes a block template and the schema stub that declares it.
Validation
| Code | Message |
|---|---|
min_items | Add at least N |
max_items | No more than N |
block_type | Block type “x” is not declared (blocks only) |
key | Item has no key / Duplicate item key |
Child fields are validated like top-level fields, with paths such as body[2].heading and stages[0].title.
Stored value and template value
A repeater or blocks value is a list of items. Each item has a _key, unique within the field and stable across edits, which is how translations find their item; a block also has _type, its block key:
[
{ "_key": "hm01", "_type": "richText", "content": [ ... ] },
{ "_key": "hm02", "_type": "quote", "text": "The best view in the valley.", "cite": "A visitor" }
] In a template, a repeater is a list of items and a blocks field a list of blocks. Each item carries its child fields as properties, key (and _key), and for a block type (and _type). Relations inside items resolve to entries as top-level ones do.
<ol>
{% for stage in entry.stages %}
<li id="stage-{{ stage.key }}"><h2>{{ stage.title }}</h2>{{ stage.notes|ptext }}</li>
{% endfor %}
</ol> With index: true on a child, its values are projected under a dotted key (stages.distanceKm), one row per item, so lists can filter documents by values inside items. In a document with several locales, which items exist and in which order is normally shared, and each locale writes its own text into them: see Localisation.
portable_text
Rich text is stored as Portable Text, a JSON array of blocks, never HTML. The schema says exactly which styles, lists and marks a field allows; the editor offers only those, and content pasted from Word, Google Docs or a web page is posted to the engine, converted and normalised to the field’s schema, with a list of what changed.
fields:
- name: description
type: portable_text
styles: [normal, h2, h3, blockquote]
lists: [bullet, number]
marks:
decorators: [strong, em, underline]
annotations:
- key: link
kinds: [{ key: page, of: document, types: [page] }, { of: url }, { of: email }]
attributes: [text, new_tab]
- key: glossary
label: { en-GB: Glossary term }
fields:
- { name: definition, type: text, required: true }
blocks:
- key: figure
label: { en-GB: Image }
fields:
- { name: image, type: asset, required: true }
- { name: caption, type: text }
max: 5000 Parameters
| Parameter | Type | Default | What it does |
|---|---|---|---|
styles | list | normal, h2, h3, h4, blockquote | The block styles allowed, from normal, h2 to h6 and blockquote. Leave h1 to the page template. |
lists | list | bullet, number | The list styles allowed. |
marks | list or mapping | strong, em and link | The inline marks allowed. See below. |
blocks | list | none | Object blocks an editor can insert between paragraphs, declared like the blocks of a blocks field (key, label, description, fields). |
max | whole number | none | The most characters of text, counted as grapheme clusters. |
Marks, short form. A list of names. strong, em, underline, code and strike-through are decorators; link, internalLink and mention are annotations:
marks: [strong, em, link] Marks, long form. A mapping with decorators and annotations, where each annotation is { key, ... }:
| Annotation | What it is |
|---|---|
{ key: link } | A link with the link field’s model. It takes kinds, attributes and rel and nothing else; a document target is stored by id, so the link survives a move. nofollow and ugc are always allowed for partners’ links. |
{ key: internalLink, to: [page, article] } | A reference to a document, picked from the given types (every type when to is left out). |
{ key: mention } | A staff user, picked by typing @; renders as <span class="mention">. |
{ key: <yours>, label, fields } | A custom annotation with fields of its own, validated like a block’s fields. A custom annotation with no fields is applied as it is. |
Validation
Validation is against the field’s schema, so a value never holds what the field does not allow:
| Code | Message |
|---|---|
style | Style “h1” is not allowed here |
list | List style “number” is not allowed here |
decorator | Formatting “underline” is not allowed here |
annotation | Annotation “x” is not allowed here |
link_marks | Text can carry one link at a time |
block_type, inline_type | Block type / Inline object “x” is not allowed here |
max_length | Must be N characters or fewer |
A rich text field with translate: none gets a warning: it cannot then be translated at all.
Stored value
[
{ "_type": "block", "_key": "b1", "style": "normal", "markDefs": [],
"children": [
{ "_type": "span", "_key": "b1s0", "text": "Open every day except ", "marks": [] },
{ "_type": "span", "_key": "b1s1", "text": "Mondays", "marks": ["strong"] }
] },
{ "_type": "figure", "_key": "f1", "image": { "asset": "3c1e..." }, "caption": "The tower" }
] A list item is a block with listItem (bullet or number) and level. A link is a markDefs entry referenced from a span’s marks. Text is stored in Unicode NFC. An import may give a rich text value as { "html": "<p>...</p>" }, converted by the same converter the editor’s paste uses (Seeding and importing).
Rich text is translated whole: each locale holds its own complete value, so a translator can merge and split sentences.
Rendering
| Filter | What it gives |
|---|---|
|ptext | The HTML. Every span is escaped. h2 to h4 headings carry ids. |
|ptext:"compact" | The HTML with the compact serialiser set for object blocks (below). |
|toc | A navigation list of the headings, linking to their ids. |
|plain | The text alone, for a meta description or a card; follow it with |truncate:160 or |excerpt:160 to shorten it. |
<div class="prose">{{ entry.description|ptext }}</div>
<nav aria-label="On this page">{{ entry.description|toc }}</nav>
<meta name="description" content="{{ entry.description|plain|excerpt:160 }}"> Object blocks render through ptext/<set>/<key>.html in the site’s template roots, where <set> is default unless the filter names one. The block’s fields are on node, its position on index, beside every global of the page:
{# templates/_shared/ptext/default/figure.html #}
{{ node.image.figure("card") }} An object block with no template renders nothing on the public site (with serve --dev, a notice naming the template to add), and engine-dmo check warns: object block "figure" of listing.description has no serialiser template for site main, so |ptext renders it as nothing. Naming a serialiser set that has no templates at all is an error when the page renders.
Relations
The relation field: references from a document to other documents, tags, smart lists, assets, organisations or people, with targets, cardinality, tag filters, publish checks, stored shape, and how templates receive resolved entries.
Localisation
How fields are translated: the translate modes (none, locale, language, group), the shared skeleton and per-locale leaves of repeaters, blocks, links and assets, structure per_locale, and field fallback on the public site.