Skip to content
On this page

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 ​

json
{
  "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": [ ... ]
}
FieldWhat it does
typeMust equal the folder name. The auto-registry uses it. Required.
categoryGroups your section in the library sidebar: text, media, cta, cart, blog, content, product. Pick the closest match.
iconAn Iconify icon name (we use the solar:* set). Browse at icon-sets.iconify.design.
previewURL 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.
titleHuman label in the library. Short — fits one line.
descriptionOne-paragraph explanation. Shown when the merchant hovers / expands the card.
pagesArray 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).
maxPerTemplate1 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 ​

jsonc
// 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 ​

PropertyApplies toWhat it does
typeAllOne of: text, longtext, switch, number, select, color, image, link, picker, richtext.
pathAllThe key (or dot.path) under config.values where the value is stored. "primaryCta.text" writes to config.primaryCta.text.
labelAllShown above the input.
defaultAllInitial value when a new instance is dropped. Also drives the type checker.
helpAllOptional explanation shown below the input. Use it generously — merchants are not engineers.
translatabletext, longtext, richtextWhen true, the merchant sees per-locale tabs and the value becomes { en: "...", fr: "...", ar: "..." }. Your component reads via useT().
optionsselectArray of { value, label }.
min, maxnumberBounds for the input.
placeholdertext, linkGreyed-out hint inside the input.
pickerResourcelinkSet to collection, product, page etc. to show the resource picker as a sidekick to the link field.
urlTemplatelinkUsed with pickerResource. "/collections/{slug}" means picking a collection writes that URL.
resourcepickerSame values as pickerResource.
multiplepickerWhen 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:

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

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

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

  1. One scope per editable element. Don't lump section + title under the same key — merchants need to recolor each independently.
  2. 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:

  1. Render your section in the demo storefront (http://localhost:3001/ or in the builder).
  2. Take a clean screenshot of the section's bounding box at desktop width. Recommended dimensions: 800x500 px (the library card crops to this aspect).
  3. Save it at souk-theme/public/sections/<Type>/preview.png — folder name must match manifest.type.
  4. Reference it in the manifest:
    json
    "preview": "https://themes.storeino.com/souk/sections/<Type>/preview.png"
    Use the prod URL here. On dev, the same path resolves automatically against 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, skipped

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

Released under the MIT License.