The schema property of a section or block definition controls the sidebar editor in Studio. It is an array of fieldsets, where each fieldset groups related fields into a collapsible panel.
schema: [
{
label: 'Content',
helpText: 'The main content of this section.',
fields: [
{ type: 'text', name: 'heading', label: 'Heading' },
{ type: 'richtext', name: 'body', label: 'Body Text' },
],
},
{
label: 'Design',
defaultOpen: true,
fields: [
{ type: 'color', name: 'backgroundColor', label: 'Background Color' },
],
},
]
Every field type shares these properties:
'text', 'select', 'media').The default property and the runtime fallback serve different purposes. Understanding the distinction prevents surprises in your component.
Default values are applied once: when an editor adds a new section or block to a page in Studio. Studio pre-fills the field with the default value. After creation, the default is not used again, even if the editor clears the field.
Runtime fallbacks are applied by Frontend Core whenever a prop has no configured value. This happens when:
default was set.The fallback is not the default value. It is a type-appropriate zero value determined by the field type:
| Field type | Fallback |
|---|---|
text, textarea, richtext | '' (empty string) |
checkbox | false (true for visibility decorators) |
select, radio, toggle_button | First option's value |
object | Object with fallbacks applied recursively to each nested field |
array | [] |
json | null |
number, icon, media, link, query, color | undefined |
String fields like text and textarea always resolve to a string, so you can use them without null checks. Fields that fall back to undefined need a guard in your template.
Single-line text input.
{ type: 'text', name: 'heading', label: 'Heading', placeholder: 'Enter a heading' }
Prop type: string · Fallback: ''
Multi-line plain text input.
{ type: 'textarea', name: 'excerpt', label: 'Excerpt', maxLength: 200 }
Prop type: string · Fallback: ''
Numeric input with optional constraints.
{ type: 'number', name: 'columns', label: 'Columns', default: 3, min: 1, max: 6, step: 1 }
'$').'px').Prop type: number | undefined · Fallback: undefined
Boolean toggle.
{ type: 'checkbox', name: 'showPrice', label: 'Show Price', default: true }
A checkbox can also act as a visibility decorator to control whether another field is shown in the sidebar.
Prop type: boolean · Fallback: false
Dropdown with predefined options.
{
type: 'select',
name: 'layout',
label: 'Layout',
default: 'full-width',
options: [
{ label: 'Full Width', value: 'full-width' },
{ label: 'Boxed', value: 'boxed' },
{ label: 'Centered', value: 'centered' },
],
}
Prop type: string · Fallback: first option's value
Radio button group. Same data shape as select, different UI.
{
type: 'radio',
name: 'alignment',
label: 'Alignment',
default: 'left',
options: [
{ label: 'Left', value: 'left' },
{ label: 'Center', value: 'center' },
{ label: 'Right', value: 'right' },
],
}
Prop type: string · Fallback: first option's value
Segmented button group. Each option can have an icon.
{
type: 'toggle_button',
name: 'sectionStyle',
label: 'Style',
default: 'full-width',
options: [
{ label: 'Full', value: 'full-width', icon: 'i-heroicons-arrows-pointing-out' },
{ label: 'Boxed', value: 'boxed', icon: 'i-heroicons-square-2-stack' },
],
}
Prop type: string · Fallback: first option's value
Icon picker. Lets editors choose from available icon sets.
{ type: 'icon', name: 'icon', label: 'Icon' }
No type-specific properties.
Prop type: string | undefined · Fallback: undefined
Displays a read-only heading or help text inside the sidebar. Not a data field; it does not produce a prop value.
{ type: 'info', name: 'designInfo', label: 'Use these settings to customize the appearance.', heading: 'Design Options' }
Prop type: none (not passed as a prop)
Rich text editor with formatting (bold, italic, links, headings, lists, etc.).
{ type: 'richtext', name: 'body', label: 'Body Text', placeholder: 'Write something...' }
The field value is an HTML string. Render it with the RichContent component from the UI Kit to get consistent typography for headings, lists, blockquotes, tables, links, and images:
<template>
<RichContent :html="body" />
</template>
Using v-html directly works but skips these styles.
Prop type: string · Fallback: ''
Color picker. Supports theme colors and optional custom color input.
{ type: 'color', name: 'backgroundColor', label: 'Background Color', allowCustom: true }
Prop type: ColorFieldValue | undefined · Fallback: undefined
Media picker for images and videos from the media library.
{ type: 'media', name: 'heroImage', label: 'Hero Image', allowedTypes: ['image'] }
The field value is a Media object containing source URLs, dimensions, and alt text. Render it with the Media component from the UI Kit:
<template>
<Media v-if="heroImage" :media="heroImage" sizes="100vw md:50vw" />
</template>
When allowedTypes is set to ['image'], the prop type narrows to MediaImage.
Prop type: MediaImage | MediaVideo | undefined · Fallback: undefined
Link picker. Editors can choose between internal pages, external URLs, page anchors, and entity references (products, categories, etc.).
{ type: 'link', name: 'ctaLink', label: 'Button Link' }
No type-specific properties.
The field value is a Link object. The link type depends on what the editor selected:
| Link variant | Description |
|---|---|
reference | A link to a product, category, or blog post by slug |
url | An external URL |
anchor | A same-page anchor (e.g. #features) |
page | A link to a specific page by ID |
pageType | A link to a page type with parameters |
Resolve a Link to a URL string with linkResolver.resolve() and pass it to an anchor or NuxtLink:
<template>
<NuxtLink v-if="ctaLink" :to="linkResolver.resolve(ctaLink)">
{{ ctaLabel }}
</NuxtLink>
</template>
Prop type: Link | undefined · Fallback: undefined
Groups nested fields into a single prop. The nested schema uses the same fieldset structure as the top-level definition schema.
{
type: 'object',
name: 'badge',
label: 'Badge',
schema: [
{
fields: [
{ type: 'text', name: 'text', label: 'Badge Text' },
{ type: 'color', name: 'color', label: 'Badge Color' },
],
},
],
}
Access nested properties on the prop directly:
<script setup lang="ts">
// ---cut---
const { badge } = defineProps<{
badge: { text: string; color: string | undefined }
}>();
</script>
<template>
<span v-if="badge.text" class="badge">
{{ badge.text }}
</span>
</template>
An object field can also act as a style decorator to attach styling controls to another field.
Prop type: Object (shape determined by nested schema) · Fallback: object with fallbacks applied recursively to each nested field
Repeatable list of items. Each item has its own set of fields defined by a nested schema.
{
type: 'array',
name: 'features',
label: 'Features',
labelSingular: 'Feature',
max: 6,
itemLabelProperty: 'title',
schema: [
{
fields: [
{ type: 'text', name: 'title', label: 'Title' },
{ type: 'icon', name: 'icon', label: 'Icon' },
{ type: 'textarea', name: 'description', label: 'Description' },
],
},
],
}
Each array item receives a stable .id property generated by Studio. Use it as the :key in v-for loops:
<script setup lang="ts">
// ---cut---
const { features } = defineProps<{
features: { id: string; title: string; icon: string | undefined; description: string }[]
}>();
</script>
<template>
<ul>
<li v-for="feature in features" :key="feature.id">
<h3>{{ feature.title }}</h3>
<p>{{ feature.description }}</p>
</li>
</ul>
</template>
Prop type: Array (each item shaped by the nested schema, plus an id property) · Fallback: []
Connects a section or block to entity data from Orchestr. In Studio, editors configure which entities the component displays. At render time, Frontend Core resolves the configured query through Orchestr and passes the result to your component.
A query field that fetches a list of products for a product slider:
{
type: 'query',
name: 'products',
label: 'Products',
entityType: 'Product',
components: [ProductBase, ProductPrices, ProductMedia],
links: {
'ecommerce/product/variants': {
entityType: 'ProductVariant',
components: [ProductVariantBase, ProductVariantAvailability],
limit: 5,
},
},
}
When singleEntity is not set (or false), the prop contains a ClientEntitySet with an entities array. Iterate over it in your template:
<script setup lang="ts">
import type { ClientEntitySet } from '@laioutr-core/orchestr/types';
// ---cut---
const { products } = defineProps<{
products: ClientEntitySet | undefined
}>();
</script>
<template>
<div v-for="product in products?.entities ?? []" :key="product.id">
<h2>{{ product.components.base.title }}</h2>
<Media
v-if="product.components.media"
:media="product.components.media.image"
/>
</div>
</template>
Set singleEntity: true when the component expects exactly one entity (e.g. a product detail page). The prop is then a single ClientEntity instead of a set:
{
type: 'query',
name: 'product',
label: 'Product',
entityType: 'Product',
singleEntity: true,
components: [ProductBase, ProductPrices, ProductDescription, ProductMedia],
links: {
'ecommerce/product/variants': {
entityType: 'ProductVariant',
components: [ProductVariantBase, ProductVariantInfo],
},
},
}
<script setup lang="ts">
import type { ClientEntity } from '@laioutr-core/orchestr/types';
// ---cut---
const { product } = defineProps<{
product: ClientEntity | undefined
}>();
</script>
<template>
<div v-if="product">
<h1>{{ product.components.base.title }}</h1>
<RichContent :html="product.components.description.body" />
<Media
v-if="product.components.media"
:media="product.components.media.image"
/>
</div>
</template>
'Product', 'Category', 'BlogPost'). Must match an entity type with registered Orchestr query handlers.true, the prop resolves to a single ClientEntity object. When false (the default), the prop resolves to a ClientEntitySet with an entities array.The entityType determines which query handlers and component resolvers are involved. The components and links properties tell Orchestr exactly what data to include, so it can fetch everything in a single pass.
The links property declares related entities that should be fetched alongside the primary query result. Each key is a link token (e.g. 'ecommerce/product/variants'), and the value describes what to fetch for that link:
links: {
'ecommerce/product/variants': {
entityType: 'ProductVariant',
components: [ProductVariantBase, ProductVariantOptions],
limit: 5,
},
'blog/collection/posts': {
entityType: 'BlogPost',
components: [BlogPostBase, BlogPostMedia],
limit: 16,
},
}
LinkRecursionError.limit values, or deeply nested link chains can slow down page rendering. Only fetch the components and links your component actually uses, and set a limit that matches what you display.In your component, access linked entities through the links property of each entity:
<script setup lang="ts">
import type { ClientEntity } from '@laioutr-core/orchestr/types';
// ---cut---
const { product } = defineProps<{
product: ClientEntity | undefined
}>();
</script>
<template>
<div v-if="product">
<div
v-for="variant in product.links['ecommerce/product/variants'].entities"
:key="variant.id"
>
{{ variant.components.base.title }}
</div>
</div>
</template>
Prop type: ClientEntitySet | ClientEntity | undefined (depends on singleEntity) · Fallback: undefined
Raw JSON editor. Use this for advanced configuration that does not fit other field types.
{ type: 'json', name: 'customConfig', label: 'Custom Configuration', placeholder: '{ "key": "value" }' }
Prop type: JSONType | null · Fallback: null
Some fields can be linked to other fields to change their presentation in the Studio sidebar. Decorators do not affect the data model; both the decorator and the target field are passed as separate props.
A checkbox with for and as: 'visibility' controls whether another field is visible in the sidebar.
// The text field
{ type: 'text', name: 'subtitle', label: 'Subtitle' },
// The visibility toggle
{ type: 'checkbox', name: 'subtitleVisible', for: 'subtitle', as: 'visibility', default: true },
When the editor unchecks the toggle, the subtitle field hides in the sidebar. Both values are still passed as props to your component. Your component can use the checkbox value to conditionally render the field:
<template>
<p v-if="subtitleVisible">{{ subtitle }}</p>
</template>
true (visible) instead of the normal false for regular checkboxes.An object with for and as: 'style' attaches styling controls to another field. Studio renders the style panel inline below the target field.
// The text field
{ type: 'text', name: 'heading', label: 'Heading' },
// The style object
{
type: 'object',
name: 'headingStyle',
label: 'Heading Style',
for: 'heading',
as: 'style',
schema: [
{
fields: [
{ type: 'color', name: 'color', label: 'Text Color' },
],
},
],
}
The style object is passed as a separate prop. Apply it in your template:
<template>
<h1 :style="{ color: headingStyle?.color }">{{ heading }}</h1>
</template>