Introduction

Section Definitions

How to create and register section definitions that appear in Laioutr Studio.

You have a Laioutr app and you want editors to place a new section on pages through Studio. Every section starts with a definition: a TypeScript object that declares the component name, Studio metadata, configurable fields, and slots for blocks.

import type { SectionDefinition } from '@laioutr-core/core-types/frontend';
declare const defineSection: <const T extends SectionDefinition>(definition: T) => T;
// ---cut---
export const definition = defineSection({
  component: 'SectionHeroBanner',
  studio: {
    label: 'Hero Banner',
    description: 'A full-width banner with heading, media, and a call-to-action button.',
    previewSrc: '/app-my-app/component-previews/SectionHeroBanner.png',
    tags: ['Heroes', 'Banner'],
  },
  slots: [
    {
      name: 'default',
      studio: { label: 'Content' },
    },
  ],
  schema: [
    {
      label: 'Content',
      fields: [
        { type: 'text', name: 'heading', label: 'Heading' },
        { type: 'media', name: 'backgroundImage', label: 'Background Image', allowedTypes: ['image'] },
        { type: 'text', name: 'ctaLabel', label: 'Button Label' },
        { type: 'link', name: 'ctaLink', label: 'Button Link' },
      ],
    },
  ],
});

The platform reads this definition at three points: Studio uses it to build the sidebar editor, Frontend Core uses it to wire up data and props, and your Vue component uses it to define its props.

Required properties

Every section definition needs these properties:

PropertyTypePurpose
componentstringThe globally registered Vue component name. Must match the component's filename (e.g., SectionHeroBanner.vue registers as 'SectionHeroBanner').
studioobjectMetadata shown in the Studio UI. At minimum, provide label.
slotsSectionSlotDefinition[]Named insertion points for blocks. Pass an empty array if the section has no slots.

schema is optional but present on almost every section.

The studio object

The studio property controls how the section appears in Studio's component picker and sidebar.

import type { SectionDefinition } from '@laioutr-core/core-types/frontend';
declare const defineSection: <const T extends SectionDefinition>(definition: T) => T;
// ---cut---
export const definition = defineSection({
  component: 'SectionImageAndContent',
  studio: {
    label: 'Image and Content',
    description: 'Combine large images or videos with any content.',
    previewSrc: '/app-ui/component-previews/SectionImageAndContent.png',
    tags: ['Banner', 'Content'],
  },
  slots: [],
  schema: [],
});
label
string required
Display name in the Studio component picker.
description
string
Short description shown below the label.
previewSrc
string
Path to a preview image. Place the image in your app's public/ directory.
tags
WellKnownComponentTag[]
Categorization tags for the component picker. Use well-known tags or any custom string.
propsWizard
PropsWizard
A multi-step wizard that pre-configures the section before it is placed on a page. See Props wizard.

Well-known tags

Studio recognizes these tags for built-in category filters:

Banner, Brands, Blog, Checkout & Cart, Content, Customer Relations, Featuring Products, Footer, Grids, Header, Heroes, Navigation, Products, Product Detail Page, Product Listing Page, Testimonials, Sliders, Blank Containers

You can also pass any custom string.

Props wizard

A props wizard lets editors choose a pre-configured variant when they add a section. Each variant sets a predefined combination of props.

import type { SectionDefinition } from '@laioutr-core/core-types/frontend';
declare const defineSection: <const T extends SectionDefinition>(definition: T) => T;
// ---cut---
export const definition = defineSection({
  component: 'SectionHeroBanner',
  studio: {
    label: 'Hero Banner',
    propsWizard: {
      steps: [
        {
          type: 'variant',
          title: 'Choose a layout',
          input: [
            {
              id: 'centered',
              label: 'Centered',
              icon: 'i-heroicons-view-columns',
              previewSrc: '/app-my-app/previews/hero-centered.png',
              props: { layout: 'centered', sectionStyle: 'full-width' },
            },
            {
              id: 'split',
              label: 'Split',
              icon: 'i-heroicons-squares-2x2',
              previewSrc: '/app-my-app/previews/hero-split.png',
              props: { layout: 'split', sectionStyle: 'boxed' },
            },
          ],
        },
      ],
    },
  },
  slots: [],
  schema: [],
});

Defining slots

Slots are named insertion points where editors place blocks. They map directly to Vue <slot> elements in your component.

import type { SectionDefinition } from '@laioutr-core/core-types/frontend';
declare const defineSection: <const T extends SectionDefinition>(definition: T) => T;
// ---cut---
export const definition = defineSection({
  component: 'SectionProductShowcase',
  studio: { label: 'Product Showcase' },
  slots: [
    {
      name: 'default',
      studio: { label: 'Content Blocks' },
    },
    {
      name: 'sidebar',
      studio: { label: 'Sidebar' },
      restrictTo: ['BlockProductCard', 'BlockPromotion'],
      prefer: ['BlockProductCard'],
    },
  ],
  schema: [],
});
name
string required
Slot name. Use 'default' for the main slot. Must match the <slot name="..."> in your Vue template.
studio.label
string
Label shown in the Studio sidebar for this slot area.
restrictTo
(string | BlockDefinition)[]
If set, only these blocks can be placed in this slot. Accepts component name strings or imported block definition objects.
allow
(string | BlockDefinition)[]
Blocks marked as isStandalone: false need to appear in this list to be usable in this slot.
prefer
(string | BlockDefinition)[]
These blocks appear first in the Studio block picker for this slot.

Pass an empty array (slots: []) if your section does not accept child blocks.

Adding a schema

The schema property defines the fields that appear in the Studio sidebar. Fields are grouped into fieldsets (collapsible panels in the sidebar UI).

schema: [
  {
    label: 'Content',
    fields: [
      { type: 'text', name: 'heading', label: 'Heading' },
      { type: 'richtext', name: 'body', label: 'Body Text' },
    ],
  },
  {
    label: 'Design',
    defaultOpen: false,
    fields: [
      { type: 'color', name: 'backgroundColor', label: 'Background Color' },
      {
        type: 'select',
        name: 'layout',
        label: 'Layout',
        default: 'full-width',
        options: [
          { label: 'Full Width', value: 'full-width' },
          { label: 'Boxed', value: 'boxed' },
        ],
      },
    ],
  },
]

Each fieldset can have a label, optional helpText, icon, and defaultOpen (defaults to true).

For the full list of available field types and their options, see Schema Fields.

Wiring the definition to a Vue component

The definition and the component live in the same .vue file. Export the definition from a regular <script lang="ts"> block, then use definitionToProps() in <script setup> to derive Vue props from it.

SectionHeroBanner.vue
<script lang="ts">
import { defineSection, definitionToProps } from '@laioutr-core/frontend-core/types';
// ---cut---
export const definition = defineSection({
  component: 'SectionHeroBanner',
  studio: { label: 'Hero Banner' },
  slots: [{ name: 'default', studio: { label: 'Content' } }],
  schema: [
    {
      label: 'Content',
      fields: [
        { type: 'text', name: 'heading', label: 'Heading' },
        { type: 'richtext', name: 'body', label: 'Body Text' },
      ],
    },
  ],
});
</script>

<script setup lang="ts">
const props = defineProps(definitionToProps(definition));
</script>

<template>
  <section>
    <h1>{{ heading }}</h1>
    <div v-html="body" />
    <slot />
  </section>
</template>

definitionToProps keeps your component's props in sync with the schema. You never declare props by hand. For sections, it also adds a slots prop (typed as an object) so you can access block data for each named slot.

File conventions

Place each section as a single .vue file in your app's sections directory:

src/runtime/app/sections/
  SectionHeroBanner.vue
  SectionImageAndContent.vue
  SectionTestimonialCarousel.vue

Register the directory in your module's registerLaioutrApp call:

// module.ts
registerLaioutrApp({
  sections: [resolve('./runtime/app/sections')],
  // ...
});

Full example

A section with media, design settings, and a content slot (based on the built-in SectionImageAndContent):

SectionImageAndContent.vue
<script lang="ts">
import { defineSection, definitionToProps, toMedia } from '#imports';

export const definition = defineSection({
  component: 'SectionImageAndContent',
  studio: {
    label: 'Image and Content',
    description: 'Combine large images or videos with any content.',
    previewSrc: '/app-ui/component-previews/SectionImageAndContent.png',
    tags: ['Banner', 'Content'],
  },
  slots: [
    {
      name: 'default',
      studio: { label: 'Content' },
    },
  ],
  schema: [
    {
      label: 'Media',
      fields: [
        {
          type: 'media',
          name: 'media',
          label: 'Media',
          allowedTypes: ['image'],
        },
        {
          type: 'select',
          name: 'imageSizeMode',
          label: 'Image Size Mode',
          default: 'keep-ratio',
          options: [
            { label: 'Keep Ratio', value: 'keep-ratio' },
            { label: 'Fill', value: 'fill' },
          ],
        },
      ],
    },
    {
      label: 'Design',
      fields: [
        {
          type: 'toggle_button',
          name: 'sectionStyle',
          label: 'Style',
          default: 'full-width',
          options: [
            { label: 'Full', value: 'full-width' },
            { label: 'Boxed', value: 'boxed' },
          ],
        },
        {
          type: 'color',
          name: 'customBackgroundColor',
          label: 'Custom Background Color',
        },
        {
          type: 'select',
          name: 'proportions',
          label: 'Proportions',
          default: '50/50',
          options: [
            { label: '50/50', value: '50/50' },
            { label: '60/40', value: '60/40' },
            { label: '70/30', value: '70/30' },
          ],
        },
        {
          type: 'number',
          name: 'sectionHeight',
          label: 'Section Height',
          default: 0,
        },
      ],
    },
  ],
});
</script>

<script setup lang="ts">
const props = defineProps(definitionToProps(definition));
</script>

<template>
  <section>
    <img v-if="media" :src="toMedia(media).src" :alt="toMedia(media).alt" />
    <div>
      <slot />
    </div>
  </section>
</template>