Skip to main content

Editable sections

How a section becomes editable in Edit mode — the companion .yml that declares its fields, every field type, and where list data lives.

Edit mode shows fields for a section because the section declares them. Each section component under resources/views/components/sections/ can carry a companion .yml with the same name — hero.blade.php and hero.yml — that lists which of the component's props are editable, and how.

Without a .yml, a section is still editable: every @props entry with a plain string default becomes a text field. The .yml is how you turn that into a good form — a textarea where copy is long, a switch for a toggle, an image picker for a picture, a list for a menu.

A section and its .yml

{{-- resources/views/components/sections/hero.blade.php --}}
@props([
    'heading' => 'Reliable service for every part of your home',
    'body' => 'Licensed, insured local pros. Book in minutes.',
    'showBadge' => '1',
    'image' => '/images/hero.webp',
    'ctaText' => 'Get a free estimate',
    'ctaLink' => '/contact',
])
<section class="…">
    @if ($showBadge)<span class="…">Same-day service</span>@endif
    <h1>{{ $heading }}</h1>
    <p>{{ $body }}</p>
    <img src="{{ $image }}" alt="">
    <a href="{{ $ctaLink }}">{{ $ctaText }}</a>
</section>
# resources/views/components/sections/hero.yml
title: Hero
description: The opening statement — headline, supporting copy, photo, and the main button.
fields:
    heading:
        type: text
        label: Headline
    body:
        type: textarea
        label: Supporting copy
        rows: 3
    showBadge:
        type: toggle
        label: Show the same-day badge
    image:
        type: image
        label: Photo
    ctaText:
        type: text
        label: Button text
    ctaLink:
        type: url
        label: Button link

Three rules make this work:

  1. Field keys are prop names. heading: in the .yml is $heading in the template.
  2. The @props default is the default. What renders when nothing has been set is always the component's own default; a default: in the .yml is only a fallback for props the component doesn't declare.
  3. Values live on the tag. When you edit a field, the page's <x-sections.hero heading="…"> gains or changes that attribute. Clearing the field removes the attribute, and the default returns.

title names the section on the canvas and in the panel; description appears under the name.

Field types

Type Control Notes
text single line the default type
textarea multi-line rows: 3 sets the height (2–20)
url single line a page path or a full address
image thumbnail + media picker the value is what goes in src
select dropdown options: { value: Label, … }
color swatch + hex
number number box min, max, step
range slider min, max, step
toggle switch on is "1", off is "" — gate with @if ($flag)
repeater a list of rows see below

Every field accepts label (defaults to the key, title-cased), description (a hint under the control), and required (a marker only — nothing is enforced).

Values are strings, because they live in tag attributes. A toggle is "1" or empty; a number is its digits.

Lists: the repeater

A list — navigation links, features, menu items, a team — never fits in an attribute, so repeater rows live in the site's data files and the component reads them as a variable:

fields:
    items:
        type: repeater
        label: Services
        source: services            # collections/services.json
        item_label: name            # which sub-field titles each row
        add_button_label: Add a service
        sub_fields:
            name:  { type: text, label: Name }
            blurb: { type: textarea, label: Blurb, rows: 2 }
            icon:  { type: image, label: Icon }
@foreach ($services as $service)
    <li>{{ $service->name }}</li>
@endforeach

Where the rows live, in order of precedence:

  1. A :bound attribute on the tag names the storage exactly: :items="$site->nav_links" edits the nav_links key of resources/data/site.json; :items="$faqs" edits resources/data/collections/faqs.json — or, for a collection stored as one file per entry, only the entry files that changed.
  2. Otherwise the field's source: — site.KEY for a site.json key, or a bare collection name.
  3. Otherwise the field key itself is taken as a collection name.

Sub-fields use the scalar types above plus richtext (an HTML string; edited as a multi-line box). A collection's own collections/NAME.yml (fields: keyed by column) adds types for columns the section doesn't declare, so a blog section picks up a content column without re-declaring the whole shape. Editing a repeater never removes columns it doesn't know about.

Set nestable: true to allow one level of children on each row — a dropdown under a navigation link, for instance. Lists cap at 100 rows.

Bound attributes are never editable

An attribute written as :heading="$site->tagline" is an expression, not a value. Edit mode leaves it out of the form rather than overwrite it with plain text. Give a section a literal attribute (or none) when you want it editable by hand.

Layouts and page settings

Layouts use the same file — components/layouts/main.yml — and their fields are what page settings shows under Visibility & SEO. title and description always appear; add image or another value the layout renders into its <head>, and it becomes a per-page setting. Public/Unlisted visibility is platform-owned, so layouts do not need to declare their own indexing field.

Sections without a component

A <section> written straight into a page, with no component, is still editable: Edit mode finds its headings, paragraphs, links, buttons, and images and offers them as fields. Any text containing Blade ({{ … }} or a directive) is left alone.

Ask Max

Max writes a .yml beside every section it builds, so websites made in conversation are editable from the start. If an older section has no fields, ask Max to "make the hero editable" — it adds the props and the .yml.