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