Field shapes reference
Manifest fields declare what the inspector shows the merchant; the actual value stored on config.values.<path> follows a shape that depends on the field type. This page lists every shape.
A section author has to know what's coming out the other side. Get this wrong and you'll render [object Object] or crash on undefined.
Text & translatable text
Manifest:
{ "type": "text", "path": "title", "label": "Title" }
{ "type": "text", "path": "title", "label": "Title", "translatable": true }Stored value:
translatable | Shape | Example |
|---|---|---|
false (or omitted) | string | undefined | "Summer sale" |
true | string | Record<string, string> | undefined | "Summer sale" (first save), then { en: "Sale", fr: "Soldes" } (after locale edit) |
When translatable: true, always read via useT() — the value may be either a string (first save before any locale-specific edit) or a locale map. useT handles both.
const title = computed(() => t(props.config?.title) || 'Default');longtext and richtext follow the same shape rule.
richtext
Manifest:
{ "type": "richtext", "path": "body", "label": "Body", "translatable": true }Stored value: HTML string (e.g. "<p>Hello <strong>world</strong></p>"). Same translatable/non-translatable shape rule as text.
Render with v-html — and trust it; the editor sanitizes on save:
<div class="prose" v-html="t(props.config?.body)" />Use a .prose wrapper (Tailwind Typography) so headings, lists, links inside the rich content style consistently.
switch
Manifest:
{ "type": "switch", "path": "showCta", "label": "Show CTA", "default": true }Stored value: boolean | undefined. Always supply a default in the manifest so missing-undefined doesn't leak as falsy where you want truthy.
const showCta = computed(() => props.config?.showCta !== false); // true unless explicitly falsenumber
Manifest:
{ "type": "number", "path": "limit", "label": "Limit", "min": 1, "max": 24, "default": 8 }Stored value: number | undefined. The inspector clamps to min/max on input but does not coerce strings — if a legacy theme stored "8" it stays a string. Defensive read:
const limit = computed(() => Number(props.config?.limit) || 8);select
Manifest:
{
"type": "select",
"path": "layout",
"default": "image-bg",
"options": [
{ "value": "image-left", "label": "Image left" },
{ "value": "image-bg", "label": "Text on image" }
]
}Stored value: one of the value strings — "image-left" | "image-bg" | undefined. Type narrow with a union:
type Layout = 'image-left' | 'image-bg';
const layout = computed<Layout>(() => props.config?.layout || 'image-bg');color
Manifest:
{ "type": "color", "path": "barColor", "label": "Bar color", "default": "#222222" }Stored value: string | undefined — hex (#222222) or rgba (rgba(0,0,0,0.5)). Render via inline style:
<div :style="{ backgroundColor: props.config?.barColor }" />Prefer tokens over color fields when the value should follow the merchant's brand. A
colorfield is for one-off accents that the merchant wants locally controllable, not for per-page brand color overrides — those areuseStylesslots.
image
Manifest:
{ "type": "image", "path": "image", "label": "Image" }Stored value: { src: string; alt?: string } | undefined. Not a plain URL string. Read defensively:
const image = computed(() => props.config?.image);
// in template:
<img v-if="image?.src" :src="image.src" :alt="image.alt || ''" />The src is a CDN URL the merchant uploaded (usually https://storeno.b-cdn.net/...). The alt is optional accessibility text — empty by default.
link
Manifest:
{
"type": "link",
"path": "primaryCta.href",
"placeholder": "/shop",
"pickerResource": "collection",
"urlTemplate": "/collections/{slug}"
}Stored value: string | undefined — a plain URL or path. The picker is sugar: when the merchant uses it, the URL is generated from urlTemplate and stored as a string. Your component never sees the picker's selection object.
<a v-if="config.primaryCta?.href" :href="config.primaryCta.href">CTA</a>pickerResource | urlTemplate example | Resulting href |
|---|---|---|
collection | /collections/{slug} | /collections/summer-sale |
product | /products/{slug} | /products/blue-shirt |
page | /pages/{slug} | /pages/about-us |
post | /posts/{slug} | /posts/launch-announcement |
Manual URLs (/shop, https://example.com, #contact) bypass the picker.
picker
Manifest:
{
"type": "picker",
"path": "productIds",
"resource": "product",
"multiple": true
}Stored value:
multiple | Shape |
|---|---|
false (or omitted) | string | undefined (a single ObjectId) |
true | string[] | undefined (an array of ObjectIds) |
The picker stores IDs only — you don't get the full product/collection object. To render data you must fetch it (see Data-driven sections).
Nested paths
path: "primaryCta.text" writes to config.primaryCta.text. path: "labels.heading" writes to config.labels.heading. Read via optional chaining:
const ctaText = computed(() => t(props.config?.primaryCta?.text) || 'Shop');
const heading = computed(() => t(props.config?.labels?.heading));The merchant clears a field → the leaf may go undefined while the parent object stays. Optional chain every level.
Arrays of objects (lists)
For "merchant can add N feature cards" patterns, manifests sometimes declare nested arrays. The convention:
{ "type": "list", "path": "cards", "label": "Cards", "itemSchema": [
{ "type": "text", "path": "title", "translatable": true },
{ "type": "image", "path": "image" }
]}Stored value: Array<{ title?: Translatable; image?: { src, alt } }>.
Render with v-for, accessing each item's fields with the same defensive shape rules:
const cards = computed(() => props.config?.cards ?? []);<div v-for="(card, i) in cards" :key="i">
<h3>{{ t(card.title) }}</h3>
<img v-if="card.image?.src" :src="card.image.src" :alt="card.image.alt || ''" />
</div>
listfields aren't shown inmanifest.mdbecause they're advanced. Look atSlider/manifest.jsonfor a real example.
What if a field is missing entirely?
A merchant on an old section instance may have data missing fields you've since added. Behavior:
- Optional field →
undefined. Optional chain everything; fall back to manifestdefaultsmentally (not at runtime; the manifest doesn't auto-fill old instances). - Required field → still
undefined. Manifest "required" is a UX cue in the inspector, not a runtime guarantee.
Always render something even when fields are empty. A blank section is a worse user experience than a placeholder.