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:
- Field keys are prop names.
heading:in the.ymlis$headingin the template. - The
@propsdefault is the default. What renders when nothing has been set is always the component's own default; adefault:in the.ymlis only a fallback for props the component doesn't declare. - 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:
- A
:boundattribute on the tag names the storage exactly::items="$site->nav_links"edits thenav_linkskey ofresources/data/site.json;:items="$faqs"editsresources/data/collections/faqs.json— or, for a collection stored as one file per entry, only the entry files that changed. - Otherwise the field's
source:—site.KEYfor a site.json key, or a bare collection name. - 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$itemsand 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, orbutton, 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@handletyped 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".