Skip to content

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:

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.

ArtifactWhat it isOwnerLifecycle
Grammar (section-types/v0.json)JSON Schema describing what a manifest looks like and the property-type vocabularycratly (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 propsthe adapter (e.g. Scavold), per websiteGenerated 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:

  1. The grammar is a versioned contract, not a runtime dependency. A JSON Schema $id is 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 with Access-Control-Allow-Origin: * so even direct fetches are unblocked.

  2. The manifest lives in the website repo. The editor reads .cratly/sections.json through 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 controls

The 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) ​

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 ​

typeEditor widgetMarkdown serializationAdapter processing
textsingle-line inputname="value"passed through
textareamulti-line inputname="value"passed through
booleancheckboxbare flag token (dark) when true, absent when falsecoerced to boolean / CSS class
numbernumeric inputname="value"coerced to number
media-filemedia pickername="path"path resolved against media_folder
page-refpage-tree pickername="path"path resolved against pages_folder
datedate pickername="2026-06-21"ISO 8601 date
datetimedate and time pickername="2026-06-21T14:30:00+02:00"ISO 8601 timestamp, offset included
enumselectname="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:

markdown
::: 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:

json
"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:

json
"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:

json
"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.svg

The 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:

json
{
  "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, no style, no href of any kind, and no value containing url( — 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):

  1. Adapter built-in section definitions (generated → .cratly/sections.json)
  2. containers declarations from .cratly.config.yaml
  3. 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:

yaml
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.