Skip to main content
MaxAI Docs

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), required (a marker only — nothing is enforced), and section (a heading the field folds under in the panel, closed until opened — section: Layout for the padding, width and colour settings a customer sets once; fields naming the same section are gathered where the first of them sits).

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 in a rich-text editor with bold, italic, headings, lists, quotes, links, and pictures). 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.

Editable on the page, too

Edit mode also lets people change a section right on the canvas — type into a heading, replace a picture, edit a link, add or reorder list items (see Editing your pages). For that, the canvas has to know which part of the page each field draws, and it learns it by reading the component's template. Write fields plainly and they work on the page as well as in the panel:

Write And the canvas can
<h2>{{ $heading }}</h2> — the field is the element's only content type into the heading
Call {{ $phone_label }} today — mixed with other words type into just those words
<img src="{{ $image }}" alt="{{ $image_alt }}"> replace the picture, edit its alt text
style="background-image: url('{{ $image }}')" replace the background picture
<a href="{{ $link }}">{{ $label }}</a> edit the link and its words
@foreach ($items as $item) over a repeater prop, with {{ $item->name }} inside do all of the above for each item, and duplicate, delete, move, reorder and add items
{{ $site->phone }}, href="{{ $site->cta_link }}" — a value from resources/data/site.json edit it, and every section that reads the same value changes with it (the panel lists these under Shared across the website)
<h3>Find us</h3>, <img src="/images/map.png"> — written into the component with no prop at all edit it in place; the words are rewritten in the component itself, so every page that uses the section changes (Other text and images in the panel)

A site.json value is offered when it is a plain string the section echoes; lists (hours, nav_links) are repeaters, and the branding keys (logo, favicon, …) belong to the Branding page. A literal is offered when it is the whole content of a heading, paragraph, link, button, list item or label — the same leaves a Custom section offers — and holds no Blade.

What stays panel-only, by design:

  • Anything the template works out — {{ Str::limit($body, 80) }}, {{ strtoupper($name) }}, {{ $title ?? 'Welcome' }}. Clicking it opens the section in the panel.
  • {!! $html !!} output.
  • A loop over anything but the prop itself — @foreach ($items->take(3) as $item) — because its positions no longer match the list's rows. Loop over $items and use @if ($loop->index < 3) inside if you need to show fewer.
  • Items of a loop nested inside the repeater's loop.
  • A field with no value: it draws nothing on the page to click.

The markers this needs are added only to the Edit-mode render of the page; your published website and shared previews never carry them.

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.

Written content: one rich-text field

A body of prose is not a form. When a section written into a page holds written content, Edit mode offers each stretch of it as one Content field with a rich-text editor instead of a field per paragraph, heading, and link.

Mark the element that holds the copy with data-richtext and that is settled:

<section class="py-16 md:py-24">
    <div data-richtext class="mx-auto max-w-3xl space-y-6 px-6">
        <h2 class="text-3xl font-bold leading-tight">Our story</h2>
        <p class="leading-relaxed">On the air since <strong>1995</strong> —
            <a href="/archives" class="underline">listen to the archives</a>.</p>
        <ul class="list-disc space-y-2 pl-6">
            <li>Saturdays, 11am to noon</li>
        </ul>
    </div>
</section>

Without the marker, a section is still treated as written content when it reads like it: a paragraph that mixes words with a link or bold text, or two or more paragraphs with about 300 characters of text between them. The first time such a section is saved from the editor, the platform adds data-richtext to the section's outer tag so it stays that way.

What can sit inside rich text:

Blocks p, h1–h6, ul, ol, li, blockquote, hr
Inline a (with href), strong, b, em, i, u, s, code, br, img (inside a paragraph, with src)
Attributes class, style, id on anything; href, target, rel, title on a link; src, alt, title, width, height, loading, decoding, srcset, sizes on an image; start, type on an ol

Your classes are kept exactly as written — the editor changes words and structure, not the design. Three things to know:

  • Anything else ends the rich text there. A div, span, table, figure, iframe, or button, a comment, an attribute not listed above, and any Blade ({{ … }}, a directive, an <x-…> component) are never handed to the editor, because it could not give them back unchanged. They split the copy: the prose before and after each becomes its own Content field, and the piece in between keeps the plain fields described above.
  • The first edit tidies the markup. The edited stretch is rewritten one block per line; <b> and <i> come back as <strong> and <em>. Everything outside that stretch is untouched, byte for byte.
  • Typed Blade is only text. {{ name }} or @handle typed into the editor is saved as the characters themselves and prints as typed.

Rich text is for copy that lives in the page. Body copy of a repeated type — posts, services — belongs in its collection's content column, typed richtext (see Data and collections).

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. Written content is the exception: Max puts a story or a page of carried-over copy straight into the page as one data-richtext block, so you edit it as a document. If a page of writing still opens as a long list of single fields, ask Max to "make this page's content one rich-text block".