Section types — schema & manifest convention
This document is the framework-agnostic contract that lets the cratly editor render the right controls (text inputs, checkboxes, media pickers, page pickers, selects) when an author inserts or edits a section in a page. It is owned by cratly, not by any single theme framework: Scavold is the reference adapter, but any tool (Next.js, Astro, Nuxt, …) may produce a conforming manifest.
The machine-readable grammar lives beside this document:
- Grammar (JSON Schema):
/schema/section-types/v0.json— canonical URLhttps://cratly.io/schema/section-types/v0.json.
The two artifacts
Section-type information is split into a grammar (the shape) and a manifest (the data), because they have different owners and different lifecycles.
| Artifact | What it is | Owner | Lifecycle |
|---|---|---|---|
Grammar (section-types/v0.json) | JSON Schema describing what a manifest looks like and the property-type vocabulary | cratly (this site) | Versioned, stable. Tools bundle the version(s) they support. |
Manifest (.cratly/sections.json) | The actual set of section kinds a given site offers and their typed props | the adapter (e.g. Scavold), per website | Generated into each website repo on build. |
Why this design avoids CORS
cratly.io and the editor are served from different hosts, so a naive "editor fetches the schema from cratly.io at runtime" approach would hit CORS. It is avoided by never making that runtime, cross-origin fetch:
The grammar is a versioned contract, not a runtime dependency. A JSON Schema
$idis an identifier, not a mandatory fetch target. The editor bundles the grammar version(s) it understands. cratly.io's copy exists for humans, agents, and third-party tooling — and, being a single public static file, may be served withAccess-Control-Allow-Origin: *so even direct fetches are unblocked.The manifest lives in the website repo. The editor reads
.cratly/sections.jsonthrough the same GitLab API path it already uses for the file tree and Markdown — not a browser fetch to cratly.io. No new cross-origin surface is introduced.
Lifecycle
adapter build (CI) website repo cratly editor
───────────────── ──────────── ─────────────
generate manifest ───emit───▶ .cratly/sections.json ──git API──▶ read & merge
from components (committed/emitted) with .cratly.config.yaml
→ render section controlsThe adapter (Scavold) is the single source of truth for its built-in section kinds: their props are generated from the component definitions, not hand-authored. The website's .cratly.config.yaml only adds custom sections and overrides labels/hints — it never restates built-in prop schemas.
Manifest format (.cratly/sections.json)
{
"$schema": "https://cratly.io/schema/section-types/v0.json",
"specVersion": 0,
"adapter": { "name": "scavold", "version": "1.4.0" },
"sections": {
"section": {
"label": "Section"
},
"video": {
"label": "Video",
"hint": "Embedded video with playback controls.",
"props": {
"src": { "type": "media-file", "label": "Video file", "required": true },
"poster": { "type": "media-file", "label": "Poster image" },
"autoplay": { "type": "boolean", "label": "Autoplay (muted)", "default": false },
"loop": { "type": "boolean", "label": "Loop" },
"muted": { "type": "boolean", "label": "Muted" },
"preload": { "type": "enum", "label": "Preload",
"values": ["none", "metadata", "auto"], "default": "metadata" },
"label": { "type": "text", "label": "Accessible label",
"hint": "Announced by screen readers when the surrounding text does not describe the video." }
}
},
"hero": {
"label": "Hero section",
"props": {
"image": { "type": "media-file", "label": "Background image" },
"dark": { "type": "boolean", "label": "Dark variant" }
}
}
}
}Property types
type | Editor widget | Markdown serialization | Adapter processing |
|---|---|---|---|
text | single-line input | name="value" | passed through |
textarea | multi-line input | name="value" | passed through |
boolean | checkbox | bare flag token (dark) when true, absent when false | coerced to boolean / CSS class |
number | numeric input | name="value" | coerced to number |
media-file | media picker | name="path" | path resolved against media_folder |
page-ref | page-tree picker | name="path" | path resolved against pages_folder |
date | date picker | name="2026-06-21" | ISO 8601 date |
datetime | date and time picker | name="2026-06-21T14:30:00+02:00" | ISO 8601 timestamp, offset included |
enum | select | name="value" | validated against values |
Serialization rule: a boolean prop is written as a bare flag on the opening line; every other type is written as a name=value pair. This mirrors how Markdown container arguments already work:
::: hero dark image=/media/banner.jpg
Content here.
:::required, default, and (for enum) values carry the rest of the editor's control metadata. See the grammar for the authoritative field list.
Explaining a section kind
Where an editor offers the section kinds to choose from, it shows each one's hint as a short explanation — one or two sentences on what the kind is for. A kind may add docs, the address of a page explaining it at length, which the editor links beside the hint and opens in a new tab:
"card": {
"label": { "*": "Card", "de": "Karte" },
"hint": { "*": "A unit of its own, set apart from the text around it.", "de": "Eine Einheit für sich, abgesetzt vom Text drumherum." },
"docs": "https://scavold.io/reference/components#scavoldcard"
}Only https: addresses are accepted; anything else is no link. Like a text, docs may be a map of locale → address with "*" as the fallback, for a page that exists in several languages.
Keeping a kind out of the choice
A site's design may have no use for some of the kinds its adapter brings — a page whose header and footer the theme draws itself, say. "offer": false keeps such a kind out of what an editor offers to choose from:
"header": { "label": "Header", "offer": false }It changes nothing else. A section of that kind already in a page is still shown with its label and properties and stays editable, and the adapter still renders it. Without the key, a kind is offered.
Localized texts
Every text an editor shows from the manifest — the label and hint of a section kind and of a property, and the label of an enum choice — is either a string or a map of locale → string, with "*" as the fallback:
"layout": {
"type": "enum",
"label": { "*": "Layout", "de": "Anordnung" },
"values": [
{ "value": "float-left", "label": { "*": "Text flowing around it, block on the left", "de": "Text umfließt, Block links" } }
]
}An editor shows the entry of the locale its interface is in, then of that locale's language (de for de-AT), then "*". A plain string is the same as a map holding only "*". An adapter that brings section kinds of its own should bring their texts in the languages its users edit in; a site overrides them in .cratly.config.yaml, in the same form.
Icons
A boolean property and the choices of an enum may name an icon. An editor then offers them as buttons showing it instead of a checkbox or a select: a boolean becomes a toggle button, and an enum becomes a group of buttons — but only when every one of its choices has an icon, because a group mixing pictures and words reads as neither.
Choosing no value is a choice too: the icon of the enum property itself is shown for it, beside the icons of its values. Without one, an editor shows that choice as text.
A site keeps its icons in one fixed folder, .cratly/icons/, one SVG file each. The file name without .svg is the icon's name, lower case, digits and dashes only. An adapter may bring icons of its own for its built-in section kinds; a site's icon of the same name replaces one of those. There is no other place an icon can come from: no path to pick, no URL to fetch.
.cratly/icons/
├── float-left.svg
└── float-right.svgThe adapter embeds the icons the manifest refers to under a top-level icons key, so the editor reads them with the manifest instead of requesting each one:
{
"specVersion": 0,
"icons": {
"float-left": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\">…</svg>",
"float-right": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\">…</svg>"
},
"sections": {
"section": {
"props": {
"float": {
"type": "enum",
"label": "First block",
"values": [
{ "value": "left", "label": "Text flows around it on the right", "icon": "float-left" },
{ "value": "right", "label": "Text flows around it on the left", "icon": "float-right" }
]
},
"framed": { "type": "boolean", "label": "Framed", "icon": "frame" }
}
}
}
}An icon carries no text, so the label is what a screen reader announces and what a tooltip shows. An adapter reports a reference to an icon that does not exist; an editor falls back to the control it would show without icons.
Draw icons in currentColor. The editor shows a button pressed, hovered or disabled by changing its colour, and an icon in a fixed colour does not follow.
Icon markup is untrusted
The markup comes from the website repository, and anyone who can push there can put anything in a file. An editor runs with its author's access to that repository, so it must not insert the markup as it is. It keeps only this subset and drops everything else:
- Elements:
svg,g,path,circle,ellipse,line,polyline,polygon,rect. Anything else is dropped with its content —script,foreignObject,style,use,image,a, animation, and gradients and patterns too. - Attributes: geometry (
viewBox,d,points,x,y,x1,y1,x2,y2,cx,cy,r,rx,ry,width,height,transform) and painting (fill,fill-rule,fill-opacity,stroke,stroke-width,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-dasharray,stroke-dashoffset,stroke-opacity,opacity,clip-rule). No event handlers, nostyle, nohrefof any kind, and no value containingurl(— which is how a reference to something else gets in.
The subset is enough for an icon set exported as plain paths, which is what icon tools produce. An icon that loses its look to it is one that relied on something outside it.
Relationship to .cratly.config.yaml
The website's .cratly.config.yaml containers block is where a site author declares custom sections and overrides editor metadata for built-ins, using the same property vocabulary as the manifest. The adapter merges, in order (last wins):
- Adapter built-in section definitions (generated →
.cratly/sections.json) containersdeclarations from.cratly.config.yaml- Explicit developer overrides passed to the adapter
The full containers.props syntax is specified in the .cratly.config.yaml spec. In short, the existing boolean-only flags: list becomes sugar for a set of boolean props, and a new props: map adds typed properties:
containers:
hero:
label: "Hero section"
props:
image: { type: media-file, label: "Background image" }
dark: { type: boolean, label: "Dark variant" }Versioning
specVersion tracks this grammar. The current version is 0 (pre-stable; breaking changes may occur). New grammar versions are published at sibling URLs (/schema/section-types/v1.json, …). Tools bundle the versions they support and warn on unknown ones.