Building a Section
This guide walks you through creating a complete section from the content block definition to the frontend Livewire component.
Overview
Section titled “Overview”A section consists of three parts:
graph LR
A[Content Block] -->|defines fields| B[Filament Builder]
B -->|stores JSON| C[Database]
C -->|resolved by| D[Livewire Component]
D -->|renders| E[Blade View]
Step 1: Create the Content Block
Section titled “Step 1: Create the Content Block”Content blocks define the form fields used in the admin panel.
namespace App\CMS\Blocks;
use Filament\Forms\Components\Builder\Block;use Filament\Forms\Components\Repeater;use Filament\Forms\Components\TextInput;use Filament\Forms\Components\Textarea;use Filament\Support\Icons\Heroicon;use JFA\FilamentCMSCore\Contracts\ContentBlock;
class Team implements ContentBlock{ public static function make(): Block { return Block::make('team') ->schema([ TextInput::make('section_title') ->required(), Textarea::make('section_description'), Repeater::make('members') ->schema([ TextInput::make('name')->required(), TextInput::make('role')->required(), Textarea::make('bio'), ]) ->columns(2), ]) ->label('Team Section') ->icon(Heroicon::OutlinedUsers); }}Block Rules
Section titled “Block Rules”Block::make('team')— The name must match the section slug- Use Filament form components (TextInput, Textarea, Repeater, etc.)
- Set
->required()on mandatory fields - Use
->icon()for visual identification in the builder
Step 2: Register the Block
Section titled “Step 2: Register the Block”Add the block class to config/filament-cms-core.php:
'content_blocks' => [ 'custom' => [ App\CMS\Blocks\Team::class, // ... other blocks ],],Step 3: Create the Livewire Component
Section titled “Step 3: Create the Livewire Component”namespace App\Livewire;
use JFA\FilamentCMSCore\CMS\SectionContent;use JFA\FilamentCMSLivewire\Livewire\SectionComponent;
class Team extends SectionComponent{ public string $sectionTitle = ''; public string $sectionDescription = ''; public array $members = [];
protected function hydrateFromContent(SectionContent $content): void { $team = $content->team;
$this->sectionTitle = $team->section_title ?? ''; $this->sectionDescription = $team->section_description ?? ''; $this->members = $team->members ?? []; }
public function render(): \Illuminate\Contracts\View\View { return view('livewire.components.team'); }}Key Rules
Section titled “Key Rules”- Extend
JFA\FilamentCMSLivewire\Livewire\SectionComponent - Implement
hydrateFromContent(SectionContent $content)(required) - Implement
resolveFieldValue(string $field): mixedonly when using visual editing (see below) - Do NOT override
mount()unless absolutely necessary - Properties are camelCase; content keys are snake_case
Visual Editing Hook
Section titled “Visual Editing Hook”For visual editing support, add the InteractsWithVisualEditing trait and implement resolveFieldValue():
use JFA\VeFilamentCMSLivewire\Concerns\InteractsWithVisualEditing;
class Team extends SectionComponent{ use InteractsWithVisualEditing;
public array $cmsSourceMap = [];
protected function hydrateFromContent(SectionContent $content): void { $team = $content->team;
$this->sectionTitle = $team->section_title ?? ''; $this->sectionDescription = $team->section_description ?? ''; $this->members = $team->members ?? []; }
protected function initializeVisualEditing(): void { $sourceMap = $this->sectionContent->team->getSourceMap(); if ($sourceMap !== null) { $this->cmsSourceMap = $sourceMap; } }
protected function getCmsSourceMap(): array { return $this->cmsSourceMap; }
protected function resolveFieldValue(string $field): mixed { return match ($field) { 'section_title' => $this->sectionTitle, 'section_description' => $this->sectionDescription, default => null, }; }}initializeVisualEditing() is called automatically by SectionComponent::mount() after hydrateFromContent(). When visual editing is unavailable, these methods are harmless no-ops.
Note: Without the
InteractsWithVisualEditingtrait, your component doesn’t needresolveFieldValue()at all. The trait is opt-in — only add it to components that need inline editing.
Step 4: Create the Blade View
Section titled “Step 4: Create the Blade View”Use renderField() for content that should be editable inline when visual editing is active:
{{-- resources/views/livewire/components/team.blade.php --}}<section class="py-20 bg-white"> <div class="container mx-auto px-4"> <h2 class="text-4xl font-bold text-center"> {!! $this->renderField('section_title', 'text') !!} </h2>
@if($sectionDescription) <p class="text-gray-600 text-center mt-4 max-w-2xl mx-auto"> {!! $this->renderField('section_description', 'textarea') !!} </p> @endif
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 mt-12"> @foreach($members as $member) <div class="text-center"> <h3 class="text-xl font-semibold">{{ $member['name'] }}</h3> <p class="text-amber-600">{{ $member['role'] }}</p> @if($member['bio']) <p class="text-gray-600 mt-2">{{ $member['bio'] }}</p> @endif </div> @endforeach </div> </div></section>Why
renderField()? It outputs plain values when visual editing is unavailable, but automatically injectsdata-cms-sourceattributes whenve-filament-cms-livewireis installed and editing mode is active. This means your components work seamlessly with or without visual editing.
Step 5: Create the Section in Admin
Section titled “Step 5: Create the Section in Admin”- Go to
/admin→ CMS → Sections - Click Create
- Fill in:
- Title: “Our Team”
- Slug:
team(must matchBlock::make('team')and component class name) - Content: Add a “Team Section” block with your content
- Status: Active
Step 6: Attach to a Page
Section titled “Step 6: Attach to a Page”- Go to CMS → Pages
- Edit a page
- Go to the Sections tab
- Attach the “Our Team” section
How Section Rendering Works
Section titled “How Section Rendering Works”sequenceDiagram
participant Page as Page Component
participant DB as Database
participant CR as ContentResolver
participant SC as SectionContent
participant Section as Team Component
Page->>DB: Load page sections
DB->>Page: Sections ordered by pivot
Page->>CR: Resolve section content
CR->>SC: Create SectionContent
SC->>Section: mount(SectionContent)
Section->>Section: hydrateFromContent()
Section->>Page: Rendered HTML
Handling Missing Content
Section titled “Handling Missing Content”Always use null coalescing for fallbacks:
protected function hydrateFromContent(SectionContent $content): void{ $team = $content->team;
// Safe — NullBlockData returns null for missing fields $this->sectionTitle = $team->section_title ?? ''; $this->members = $team->members ?? [];}For views that expect iterables:
// In blade@foreach($members ?? [] as $member) // ...@endforeachRepeater Content
Section titled “Repeater Content”Repeater fields store arrays of objects:
{ "members": [ {"name": "Alice", "role": "Developer", "bio": "..."}, {"name": "Bob", "role": "Designer", "bio": "..."} ]}Access in PHP:
$this->members = $team->members ?? [];// Result: [['name' => 'Alice', 'role' => 'Developer', ...], ...]Multiple Block Types in One Section
Section titled “Multiple Block Types in One Section”A section can contain multiple content blocks:
protected function hydrateFromContent(SectionContent $content): void{ // Section has both 'heading' and 'paragraph' blocks $this->title = $content->heading->content ?? '';
// Get all paragraphs $paragraphs = $content->all('paragraph'); $this->body = collect($paragraphs)->map( fn ($p) => $p->content )->implode("\n\n");}Troubleshooting
Section titled “Troubleshooting”| Problem | Cause | Solution |
|---|---|---|
| Section not rendering | Wrong slug | Match slug to component class name |
| Empty content | Wrong block type | Match type in JSON to Block::make() name |
[object Object] | Nested arrays | Use flat strings, not [["content" => "value"]] |
| foreach() error | Missing repeater | Use $team->members ?? [] fallback |
renderField() error | Missing InteractsWithVisualEditing trait | Add the trait and implement resolveFieldValue() |