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), 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:
- 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 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.