Skip to content

ADR 0001 — Section-type schema: ownership, location, and delivery

  • Status: Accepted
  • Date: 2026-06-17
  • Context repos: cratly.io site (this repo), scavold, editor

Problem

The cratly editor needs to render the right controls (text inputs, checkboxes, media pickers, page pickers, selects) when an author inserts or edits a section in a page. To do that it must know, per section kind, which typed properties that kind accepts. cratly is meant to stay framework-agnostic — Scavold/VitePress is only the reference way to build a site — so the editor must not bake in framework-specific knowledge.

Decision

Split the information into two artifacts with different owners:

  1. Grammar (meta-schema) — a JSON Schema describing the manifest shape and the property-type vocabulary. Owned by cratly, published at https://cratly.io/schema/section-types/v0.json. Stable and versioned. Tools bundle the version(s) they support rather than fetching it at runtime.

  2. Manifest (.cratly/sections.json) — the actual section kinds and typed props for one website. Produced by the adapter (Scavold is the reference adapter) and emitted into each website repo at build time. The editor reads it through the GitLab API — the same path it already uses for the file tree and Markdown.

The canonical grammar and the framework-agnostic spec live in this repo (the cratly.io site), not in scavold or editor, so adapter authors depend on a neutral, stable source rather than another tool's release cycle.

Why CORS is not a blocker

The editor and cratly.io are served from different hosts (the editor runs at the root of its own host — not under an /editor/ prefix — and the site will be cratly.io). A naive "editor fetches the schema from cratly.io at runtime" design would hit CORS. We avoid it by never making that runtime cross-origin fetch:

  • The grammar is bundled into the editor (a JSON Schema $id is an identifier, not a mandatory fetch target). The public copy on cratly.io is for humans, agents, and third-party tooling, and may additionally be served with Access-Control-Allow-Origin: *.
  • The per-site manifest comes from the website repo via the GitLab API, not a fetch to cratly.io.

Delivery of the manifest

The manifest is a committed generated artifact, handled like a lockfile:

  • The adapter build writes .cratly/sections.json (skipping identical re-writes); the developer commits it when it changes. The editor always reads the committed file from the repo branch — no runtime generation, no CI write-back, no bot tokens.
  • CI verifies rather than writes: after building, CI fails if the working tree shows the manifest is out of date (git status --porcelain .cratly/sections.json). This keeps the committed file honest without granting CI push access. Reference implementation: the build job in cratly/test-website's .gitlab-ci.yml.

Consequences

  • Built-in section prop schemas are generated from Scavold components, never hand-copied into site config. A site's .cratly.config.yaml only declares custom sections and overrides labels/hints; the adapter merges built-ins ⊕ config ⊕ explicit overrides.
  • Follow-up work (tracked in the README): the .cratly.config.yaml spec was moved out of scavold/CRATLY-CONFIG.md into this repo with the typed props: map added; the editor docs were moved into this VitePress site; Scavold gained manifest generation (lib/sectionManifest.js). Remaining: wire the GitLab project and deployment.

Editor-bundled default section library

A per-site manifest is optional. A site that has never been built by a conforming adapter — or whose committed manifest is stale — would otherwise leave the editor with no knowledge of even the most common section kinds. To avoid that, cratly ships an adapter-neutral default section library that editors bundle:

  • Canonical file: /schema/section-types/defaults-v0.json. It is itself a valid manifest (conforms to the v0 grammar) but carries no adapter block — it belongs to cratly, not to any framework.
  • It covers only what is framework-agnostic: the seven HTML sectioning containers (section, article, aside, header, footer, main, nav) with no props, and video with the parameters intrinsic to an HTML <video> element (src, poster, autoplay, loop, muted, controls, preload, label).
  • Framework-specific parameters stay out of it. For example Scavold's overlay (render the video as a background with the container's content on top) is a Scavold rendering concept, not an HTML <video> property — it appears only in the per-site manifest that Scavold writes, never in the neutral defaults.

Resolution order in the editor (last wins, per section kind):

  1. bundled defaults-v0.json — always present;
  2. per-site .cratly/sections.json manifest — authoritative when committed (a Scavold site's video therefore gains overlay);
  3. .cratly.config.yaml container names — only consulted when no manifest is present, and only to surface custom container names (without typed props).

This keeps the editor framework-agnostic: the default knowledge lives in cratly, not in the editor's own code, and a framework's own additions still flow exclusively through its manifest.

Delivery (follow-up): for now each editor keeps a committed copy of defaults-v0.json, refreshed from this repo by a small sync script. This mirrors the "bundle, don't fetch" rule. TODO: publish the defaults (and ideally the bundled grammar) as a versioned npm package (e.g. @cratly/section-types) that the editor and other tools depend on, replacing the vendored copy + sync script. Tracked until the cratly GitLab group and its publish pipeline exist.

See also