Writing the manifest
A manifest is one JSON file. It describes:
- Where the section appears in the builder library
- What fields the merchant can edit
- What the starting values are
- Which page templates allow the section
- Which named "element scopes" the merchant can recolor independently
The full reference, in order.
Top-level fields
{
"type": "MyBanner",
"category": "media",
"icon": "solar:gallery-wide-linear",
"preview": "https://themes.storeino.com/souk/sections/MyBanner/preview.png",
"title": "Banner",
"description": "Image + headline + CTA. Use for promo blocks.",
"pages": ["home", "shop"],
"maxPerTemplate": null,
"schema": [ ... ],
"defaults": { ... },
"aiHints": { "keywords": [...], "examples": [...] },
"styleKeys": [ ... ]
}| Field | What it does |
|---|---|
type | Must equal the folder name. The auto-registry uses it. Required. |
category | Groups your section in the library sidebar: text, media, cta, cart, blog, content, product. Pick the closest match. |
icon | An Iconify icon name (we use the solar:* set). Browse at icon-sets.iconify.design. |
preview | URL to a PNG that shows in the library card. Path is themes.storeino.{world|com}/souk/sections/<Type>/preview.png — see Section preview images below for how to produce one. |
title | Human label in the library. Short — fits one line. |
description | One-paragraph explanation. Shown when the merchant hovers / expands the card. |
pages | Array of templates the section is allowed on. Values: home, shop, product, cart, wishlist, blog, post, collections, collection, thanks, account, error, page. Use ["any"] for sections that work everywhere (Banner, Heading). |
maxPerTemplate | 1 for sections that should only exist once per page (the entire product detail, the cart items list). null for unlimited (banners, headings). |
The schema array — editable fields
Each entry in schema becomes one row in the inspector panel.
Common field types
// Plain text
{ "type": "text", "path": "title", "label": "Title", "translatable": true, "default": "New banner" }
// Long text (textarea)
{ "type": "longtext", "path": "body", "label": "Body", "translatable": true }
// Switch / boolean
{ "type": "switch", "path": "overlay", "label": "Show overlay", "default": true,
"help": "Optional help text shown under the field." }
// Number with bounds
{ "type": "number", "path": "limit", "label": "Items per row", "min": 1, "max": 12, "default": 4 }
// Single-select dropdown
{
"type": "select",
"path": "layout",
"label": "Layout",
"default": "image-bg",
"options": [
{ "value": "image-left", "label": "Image left, text right" },
{ "value": "image-bg", "label": "Text on image" }
]
}
// Color picker
{ "type": "color", "path": "bg", "label": "Background", "default": "#ffffff" }
// Image upload
{ "type": "image", "path": "image", "label": "Image",
"help": "Optional help text. Mention what aspect ratio works best." }
// Link / URL with optional resource picker
{
"type": "link",
"path": "primaryCta.href",
"label": "Button link",
"placeholder": "/shop",
"pickerResource": "collection",
"urlTemplate": "/collections/{slug}"
}
// Resource picker (one or many)
{
"type": "picker",
"path": "productIds",
"label": "Featured products",
"resource": "product",
"multiple": true,
"help": "Leave empty to auto-fetch the latest products."
}Field properties
| Property | Applies to | What it does |
|---|---|---|
type | All | One of: text, longtext, switch, number, select, color, image, link, picker, richtext. |
path | All | The key (or dot.path) under config.values where the value is stored. "primaryCta.text" writes to config.primaryCta.text. |
label | All | Shown above the input. |
default | All | Initial value when a new instance is dropped. Also drives the type checker. |
help | All | Optional explanation shown below the input. Use it generously — merchants are not engineers. |
translatable | text, longtext, richtext | When true, the merchant sees per-locale tabs and the value becomes { en: "...", fr: "...", ar: "..." }. Your component reads via useT(). |
options | select | Array of { value, label }. |
min, max | number | Bounds for the input. |
placeholder | text, link | Greyed-out hint inside the input. |
pickerResource | link | Set to collection, product, page etc. to show the resource picker as a sidekick to the link field. |
urlTemplate | link | Used with pickerResource. "/collections/{slug}" means picking a collection writes that URL. |
resource | picker | Same values as pickerResource. |
multiple | picker | When true, the picker stores an array. |
The defaults object
The values a brand-new instance starts with. Shape must match the field paths in schema:
"defaults": {
"title": "New banner",
"layout": "image-bg",
"primaryCta": { "text": "Shop now", "href": "/shop" }
}The merchant can change anything afterwards. Pick defaults that demonstrate the section well — if a Banner with empty title looks broken, your default should fill in something believable.
aiHints — feeding the AI generator
The AI theme/page generators read this when deciding whether to include your section in a generated layout.
"aiHints": {
"keywords": ["banner", "promo", "category", "feature"],
"examples": []
}Keep keywords short and concrete. The examples field is reserved for future use — leave empty.
styleKeys — element-scope styling
Optional. Lists the named scopes the merchant can recolor independently in the Style panel.
"styleKeys": [
{ "key": "section", "label": "Whole section wrapper (background, padding)" },
{ "key": "title", "label": "Title (color, size, weight)" },
{ "key": "body", "label": "Body text (color, size)" },
{ "key": "primaryCta", "label": "Primary button (background, text, border)" }
]Each key here must match a useStyles('<key>', ...) call in your component. The label is what shows in the dropdown.
Two rules:
- One scope per editable element. Don't lump section + title under the same key — merchants need to recolor each independently.
- Order by significance. The first entry is what the merchant sees by default. Put the most-edited element first.
Section preview images
The library card for your section shows a small PNG. It comes from public/sections/<Type>/preview.png in souk-theme, uploaded to BunnyCDN at deploy time.
To produce one:
- Render your section in the demo storefront (
http://localhost:3001/or in the builder). - Take a clean screenshot of the section's bounding box at desktop width. Recommended dimensions: 800x500 px (the library card crops to this aspect).
- Save it at
souk-theme/public/sections/<Type>/preview.png— folder name must matchmanifest.type. - Reference it in the manifest:jsonUse the prod URL here. On dev, the same path resolves automatically against
"preview": "https://themes.storeino.com/souk/sections/<Type>/preview.png"themes.storeino.world/souk/...because of how the BunnyCDN zones are configured.
Until the file ships, the library shows a generic placeholder. Not a blocker for merging — but always ship a preview before the section is considered done. Merchants browse the library by sight.
Validating your manifest
After saving the file, restart npm run dev. If the manifest has a syntax error or unknown field type, you'll see a warning in the dev console:
[sectionRegistry] manifest at .../MyBanner/manifest.json has no .type, skippedThe builder's section library refreshes from the manifest index, which is regenerated by npm run manifest (auto-invoked by dev/build). After editing a manifest, a hard refresh of the builder (Ctrl+Shift+R) usually picks up the new schema.
What's next
Open Testing in the builder to take your new section for a spin.