Authoring blocks
A block type is a YAML file (the content contract) plus a default partial (the presentation).
Block YAML anatomy
# teaser.yaml
handle: teaser # required; a plain slug (a vendor:name handle never renders)
name: Teaser # text or lang key; defaults to the handle
icon: icon-star
group: components # inserter group
partial: teaser # partial file name; defaults to the handle
container: false # true = holds child blocks (section, grid)
allowChildren: null # containers: handle whitelist for children (null = any)
childOnly: false # true = only insertable inside a container
fullBleed: false # true = spans the viewport; a host's rootWidth never boxes it in
defaultChildren: null # containers: starter children, e.g. [{ type: heading, data: { text: Hi } }]
defaultSettings: # generic settings preset on insert (scalars)
spacing_top: lg
fields: # content fields, October form field syntax
image:
label: Image
type: mediafinder
mode: image
title:
label: Title
type: text
link:
label: Link
type: pagefinder
translatable: false # opt out of AI translationfields may not contain a nested contenteditor. allowChildren is enforced in the editor and on save, childOnly in the editor. AI Translate sends every non-empty string of a text, textarea, richeditor, richtext or markdown field, repeater items included; a field without type counts as text. Set translatable: false on a text field that holds no copy, such as a CSS class, a form field name, coordinates or a price. Other field types (dropdowns, switches, colors, icons, media, page links, numbers) are never translated, and neither is a text field named url, href, link, src, id, anchor, class, css, icon, color, email, phone, lat, lng, slug, embed or ending in one of them after an underscore (button_url). Blocks without a definition fall back to the field names text, html, caption, alt, title, author and label.
The default partial
Every block ships <sourcePath>/<partial>.htm. A definition whose partial is missing or whose YAML fails to parse is skipped with a logged warning. The partial renders in a standalone Twig environment with:
| Name | Contents |
|---|---|
data | the block's content fields |
settings | the block's generic settings |
children | pre-rendered child HTML; containers output {{ children|raw }} |
attributes | ready-made class="…" id="…" string built from the settings |
blockId | stable document id ('' in legacy documents), for deterministic element ids |
preview | true in the editor preview |
locale | locale code of the render site |
media filter | media path → URL |
rc_resize(w, h, options) filter | resized image URL; the plain media URL without a resizer |
rc_embed filter | YouTube/Vimeo URL → privacy embed URL, '' if unrecognised |
rc_url filter | passes http, https, mailto, tel and relative links; resolves october:// to the active site's URL |
rc_css_url filter | escapes a URL for CSS url() |
rc_icon(name, size, class) | inline Lucide SVG, empty for unknown names |
rc_t(key, replace, locale) | a renick.content::lang string |
rc_image_size(path) | [width, height] of a media image, or null |
rc_filesize(path) | readable media file size |
rc_map_csv(path) | marker rows from a media CSV |
rc_entries(section, options) | published Tailor entries; options limit (1–24, default 6), order, direction, with |
rc_page_url(page, params) | CMS page URL |
rc_tailor_submission_form(data) | form model of the Submission form block |
{# teaser.htm #}
<a {{ attributes|raw }} href="{{ data.link|rc_url }}">
<img src="{{ data.image|rc_resize(600, 400) }}" alt="">
<h3>{{ data.title }}</h3>
</a>Registration sources
On a handle collision the plugin wins, then the theme, then the first event listener. Themes restyle blocks with partial overrides, not by redefining them.
- Plugin:
plugins/renick/content/blocks/<handle>/<handle>.yaml+.htm. - Theme:
themes/<theme>/content-blocks/<handle>/<handle>.yaml+.htm. A child theme inherits its parent's blocks and partial overrides, nearest theme first, so a Marketplace<theme>-childkeeps them. - Event: other plugins return configs with a
sourcePath(the partial's directory). A config without it is skipped with a warning.
Event::listen('renick.content.registerBlocks', fn () => [[
'handle' => 'pricing-table',
'name' => 'Pricing table',
'group' => 'components',
'fields' => ['plans' => ['label' => 'Plans', 'type' => 'textarea']],
'sourcePath' => plugins_path('acme/shop/blocks/pricing-table'),
]]);Field types
Values are stored as strings, except repeater and tailorfields (row arrays) and checkbox and switch (booleans). A field's default is written into data on insert. row: <name> on consecutive fields shows them side by side. A field's trigger (October's action: show|hide, field, condition: value[a][b]|checked|unchecked) shows or hides it in the Inspector depending on a sibling field. Blocks of an unregistered type keep their data and render a placeholder.
| Type | Notes |
|---|---|
text | single line; also used for unknown types |
textarea, richeditor | multi-line text |
number | min, max, step |
dropdown | options |
checkbox, switch | boolean |
colorpicker | hex color, clearable |
mediafinder | media library path |
pagefinder | october:// reference or a typed URL; singleMode, allowCustomUrl (default true), allowedTypes, excludedTypes. Render with rc_url |
iconpicker | Lucide icon name for rc_icon() |
taglist | tags, stored as one string |
repeater | sortable rows of sub-fields |
tailorcolumn | a column of the Tailor blueprint picked in the dependsOn field; kind: sortable (default), image, text |
tailorfields | fields of the submission blueprint picked in dependsOn, stored as name/label rows |
Localizing labels
name and each field's label, comment, commentAbove, emptyOption and option labels (repeater sub-fields included) may be lang keys. They resolve server-side to the backend locale: a :: key through trans(), a dotted key without spaces through the theme's JSON translations when it exists. Other strings pass through. :: keys also work in defaultChildren data.