Skip to content
On this page

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:

json
{ "type": "text", "path": "title", "label": "Title" }
{ "type": "text", "path": "title", "label": "Title", "translatable": true }

Stored value:

translatableShapeExample
false (or omitted)string | undefined"Summer sale"
truestring | 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.

ts
const title = computed(() => t(props.config?.title) || 'Default');

longtext and richtext follow the same shape rule.

richtext ​

Manifest:

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

vue
<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:

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

ts
const showCta = computed(() => props.config?.showCta !== false); // true unless explicitly false

number ​

Manifest:

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

ts
const limit = computed(() => Number(props.config?.limit) || 8);

select ​

Manifest:

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

ts
type Layout = 'image-left' | 'image-bg';
const layout = computed<Layout>(() => props.config?.layout || 'image-bg');

color ​

Manifest:

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

vue
<div :style="{ backgroundColor: props.config?.barColor }" />

Prefer tokens over color fields when the value should follow the merchant's brand. A color field is for one-off accents that the merchant wants locally controllable, not for per-page brand color overrides — those are useStyles slots.

image ​

Manifest:

json
{ "type": "image", "path": "image", "label": "Image" }

Stored value: { src: string; alt?: string } | undefined. Not a plain URL string. Read defensively:

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

Manifest:

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

vue
<a v-if="config.primaryCta?.href" :href="config.primaryCta.href">CTA</a>
pickerResourceurlTemplate exampleResulting 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:

json
{
  "type": "picker",
  "path": "productIds",
  "resource": "product",
  "multiple": true
}

Stored value:

multipleShape
false (or omitted)string | undefined (a single ObjectId)
truestring[] | 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:

ts
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:

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

ts
const cards = computed(() => props.config?.cards ?? []);
vue
<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>

list fields aren't shown in manifest.md because they're advanced. Look at Slider/manifest.json for 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 manifest defaults mentally (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.

Released under the MIT License.