Skip to main content

Component Contract Schema — v0.0.1

A component contract is a JSON file that describes a frontend component to the sloth system. The sloth Strapi plugin reads contracts to auto-generate Strapi content-type fields, and the Puck editor reads them to render the admin UI.

Canonical schema URL (use as $schema):

https://phuhh98.github.io/sloth/schemas/component-contract/0.0.1/schema.json

See the Schemas page for the hosted link and versioning conventions.


Minimal example

{
"$schema": "https://phuhh98.github.io/sloth/schemas/component-contract/0.0.1/schema.json",
"name": "hero-banner",
"label": "Hero Banner",
"kind": "section",
"schemaVersion": "0.0.1",
"dataset": [
{
"key": "headline",
"label": "Headline",
"type": "string",
"required": true
}
]
}

Top-level fields

$schema

RequiredNo
Typestring

URL of the JSON Schema used for validation. Set this to the hosted schema URL to get IDE autocomplete and inline validation.


name

RequiredYes
Typestring — kebab-case pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$

Unique component identifier across the system. Used as the primary key in Strapi content-types and the Puck editor component palette.

"name": "product-card"

label

RequiredYes
TypestringminLength: 1

Human-readable name shown in the Puck editor's component drawer and in the Strapi admin UI.


kind

RequiredYes
Typestring — enum: "layout" | "section" | "block"

Determines the structural role of the component:

ValueDescription
layoutTop-level container. Must have layoutConfig. Two modes: open-canvas (no zones) — a single implicit drop area where blocks wrap freely; zoned (zones present) — named DropZones with declared column spans.
sectionFull-width standalone component, or placed inside a layout zone with sectionConfig.
blockLeaf component placed inside a layout zone (or an open-canvas layout). Must have blockConfig.

schemaVersion

RequiredYes
Typeconst: "0.0.1"

Contract format version. The schema file itself locks this to "0.0.1" — any other value fails validation. The plugin uses this to select the correct compatibility layer.

"schemaVersion": "0.0.1"
tip

When the schema file sets "default": "0.0.1", editors that support JSON Schema defaults will auto-insert this value for you.


category

RequiredNo
Typestring

Puck editor category for grouping components in the component drawer. Example: "Content", "Media", "Navigation".


renderMeta

RequiredNo
Typeobject

Frontend-only metadata. The plugin ignores this field; the renderer uses it to mount the correct React component.

Sub-fieldRequiredDescription
rendererKeyYesKey the frontend component map uses to look up the React component.
"renderMeta": {
"rendererKey": "HeroBanner"
}

layoutConfig

Required when kind is "layout". Defines the 12-column Tailwind grid, optional full-width override, per-breakpoint responsive behaviour, and optionally named drop zones.

Layout modes:

ModeHowBehaviour
Open-canvasOmit zonesSingle implicit drop area spanning all columns. Blocks are placed freely and wrap automatically (Tailwind flex-wrap / grid auto-flow). Use for generic containers where placement order is flexible.
ZonedProvide zones array (minItems: 1)One or more named Puck DropZones, each with a declared span. Blocks target a specific zone by its key.

columns

RequiredYes
Typeinteger — 1 to 12

Number of grid columns. Maps to Tailwind grid-cols-{n}.

fullWidth

RequiredNo
Typeboolean

When true, the layout expands to full viewport width (w-full), ignoring the column grid. Use for header and footer layouts.

gap

RequiredNo
Typeenum: "none" | "xs" | "sm" | "md" | "lg" | "xl"

Default gap between zones. Tailwind class mapping:

ValueTailwind classSize
nonegap-00 px
xsgap-14 px
smgap-28 px
mdgap-416 px (default)
lggap-624 px
xlgap-832 px

responsive

Array of per-breakpoint override objects. Omit an entry to inherit the previous breakpoint's values (mobile-first).

Each item:

FieldRequiredDescription
breakpointYes"mobile" (< 768 px, no Tailwind prefix), "tablet" (≥ 768 px, md:), "desktop" (≥ 1280 px, xl:)
behaviorYes"default", "wrap", "stack-left", or "stack-right" (see table below)
columnsNoOverride column count at this breakpoint
gapNoOverride gap at this breakpoint

Behavior values:

ValueEffect
defaultInherit previous breakpoint
wrapZones wrap to next row when they don't fit
stack-leftLeftmost zone lifts to top (order-first / col-span-full)
stack-rightRightmost zone lifts to top (order-last / col-span-full)

zones

Optional. Named Puck DropZones with column-span assignments. Omit entirely for open-canvas mode. When present, must contain at least one entry (minItems: 1) — an empty array [] is not valid.

FieldRequiredDescription
keyYesFree identifier, e.g. "main", "sidebar". Referenced by the renderer.
spanYesColumns this zone occupies at default breakpoint (col-span-{n})

The sum of spans should not exceed columns.

:::tip Open-canvas vs zoned If your layout should accept any block without fixed columns (e.g. a 12-col free-form canvas), simply omit zones. The renderer will render one implicit DropZone and let blocks wrap across the full grid. Add zones only when you need distinct, independently addressable regions. :::

Example — 2-column layout with sidebar:

"layoutConfig": {
"columns": 12,
"gap": "md",
"responsive": [
{ "breakpoint": "mobile", "behavior": "stack-left" },
{ "breakpoint": "tablet", "behavior": "default" }
],
"zones": [
{ "key": "main", "span": 8 },
{ "key": "sidebar", "span": 4 }
]
}

What this looks like at each breakpoint:


blockConfig

Required when kind is "block". Defines how many columns and rows the block occupies within its parent zone.

Zone stacking and reordering is controlled by the parent layout's layoutConfig.responsive — blocks do not repeat it.

FieldRequiredDescription
colSpanYesColumns occupied in parent zone (col-span-{n})
rowSpanNoRows spanned (row-span-{n}). Omit to wrap naturally.
responsiveNoArray of { breakpoint (required), colSpan?, rowSpan? } overrides

Example:

"blockConfig": {
"colSpan": 6,
"responsive": [
{ "breakpoint": "mobile", "colSpan": 12 },
{ "breakpoint": "tablet", "colSpan": 6 }
]
}

sectionConfig

Optional. Provide only when the section is placed inside a layout zone so the renderer knows its column width. Omit entirely for standalone full-width sections (the renderer defaults to col-span-full).

FieldRequiredDescription
colSpanYes (if present)Columns in the parent zone (col-span-{n})
responsiveNoArray of { breakpoint (required), colSpan? } overrides

dataset

Required. Array of data field definitions that the plugin generates as Strapi fields. Must have at least one item.

Each item:

FieldRequiredDescription
keyYesField identifier (^[a-zA-Z][a-zA-Z0-9_]*$). Becomes the key in the flat page response map.
labelYesHuman-readable label in the Strapi admin UI and Puck editor.
typeYesOne of "string", "number", "option", "relation", "dynamic"
requiredNoWhen true, Strapi marks the field required.
valueNoDefault value pre-populated in the Puck editor.
optionsConditionally requiredRequired when type is "option". Array of { label, value }.
relationConfigConditionally requiredRequired when type is "relation". See below.

type values

ValueStrapi field generatedDescription
stringText fieldFree-form text
numberNumber fieldNumeric input
optionEnumeration fieldDropdown from options[]
relationRelation fieldReferences content-type entries
dynamicTBDReserved for future dynamic rendering

options (for type: "option")

{
"key": "size",
"label": "Size",
"type": "option",
"options": [
{ "label": "Small", "value": "sm" },
{ "label": "Medium", "value": "md" },
{ "label": "Large", "value": "lg" }
]
}

relationConfig (for type: "relation")

FieldRequiredDescription
contentTypeYesStrapi content-type UID, e.g. "api::article.article"
resolveYes"scalar" or "documentId" (see below)
pathConditionally requiredRequired when resolve is "scalar". Dot-notation path to a scalar field on the related entry.
multipleNotrue to return an array of values instead of a single value.

Resolution modes:

resolveWhat the flat map value contains
scalarThe value at path extracted from the related entry, e.g. "My Article Title". Array of values when multiple: true.
documentIdThe raw Strapi documentId string. Renderer is responsible for fetching the full entry. Array of IDs when multiple: true.

Scalar example — extract the title of a related article:

{
"key": "featuredArticle",
"label": "Featured Article",
"type": "relation",
"relationConfig": {
"contentType": "api::article.article",
"resolve": "scalar",
"path": "title"
}
}

documentId example — pass the ID, let the renderer fetch the rest:

{
"key": "relatedProducts",
"label": "Related Products",
"type": "relation",
"relationConfig": {
"contentType": "api::product.product",
"resolve": "documentId",
"multiple": true
}
}

Full example — open-canvas layout

A 12-column layout where blocks are placed freely without named zones:

{
"$schema": "https://phuhh98.github.io/sloth/schemas/component-contract/0.0.1/schema.json",
"name": "free-canvas",
"label": "Free Canvas",
"kind": "layout",
"schemaVersion": "0.0.1",
"category": "Layout",
"layoutConfig": {
"columns": 12,
"gap": "md",
"responsive": [
{ "breakpoint": "mobile", "behavior": "wrap" },
{ "breakpoint": "tablet", "behavior": "default" }
]
},
"dataset": [{ "key": "title", "label": "Section Title", "type": "string" }],
"renderMeta": {
"rendererKey": "FreeCanvas"
}
}

Full example — layout with two zones

{
"$schema": "https://phuhh98.github.io/sloth/schemas/component-contract/0.0.1/schema.json",
"name": "two-col-layout",
"label": "Two Column Layout",
"kind": "layout",
"schemaVersion": "0.0.1",
"category": "Layout",
"layoutConfig": {
"columns": 12,
"gap": "md",
"responsive": [
{ "breakpoint": "mobile", "behavior": "stack-left" },
{ "breakpoint": "tablet", "behavior": "default" }
],
"zones": [
{ "key": "main", "span": 8 },
{ "key": "sidebar", "span": 4 }
]
},
"dataset": [{ "key": "title", "label": "Section Title", "type": "string" }],
"renderMeta": {
"rendererKey": "TwoColLayout"
}
}

Full example — block component

{
"$schema": "https://phuhh98.github.io/sloth/schemas/component-contract/0.0.1/schema.json",
"name": "article-teaser",
"label": "Article Teaser",
"kind": "block",
"schemaVersion": "0.0.1",
"category": "Content",
"blockConfig": {
"colSpan": 4,
"responsive": [
{ "breakpoint": "mobile", "colSpan": 12 },
{ "breakpoint": "tablet", "colSpan": 6 },
{ "breakpoint": "desktop", "colSpan": 4 }
]
},
"dataset": [
{ "key": "title", "label": "Title", "type": "string", "required": true },
{
"key": "category",
"label": "Category",
"type": "option",
"options": [
{ "label": "News", "value": "news" },
{ "label": "Tech", "value": "tech" }
]
},
{
"key": "article",
"label": "Article",
"type": "relation",
"relationConfig": {
"contentType": "api::article.article",
"resolve": "scalar",
"path": "slug"
}
}
],
"renderMeta": {
"rendererKey": "ArticleTeaser"
}
}

Validate with the CLI

sloth contracts verify --file ./my-component.json --version 0.0.1

See CLI Command Reference for exit codes and error types.