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') ->label('Section Title') ->required(), Textarea::make('section_description') ->label('Section Description'), Repeater::make('members') ->label('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 - Set
->label()on all fields — labels are auto-extracted byBlockSchemaExtractor - 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\FilamentCMSLivewire\Livewire\SectionComponent;
class Team extends SectionComponent{ public string $sectionTitle = ''; public string $sectionDescription = ''; public array $members = [];
// No hydrateFromContent() needed — autoHydrate() handles it! // autoHydrate() maps snake_case keys to camelCase properties automatically
public function render(): \Illuminate\Contracts\View\View { return view('livewire.components.team'); }}How autoHydrate Works
Section titled “How autoHydrate Works”The base SectionComponent::mount() calls autoHydrate() which:
- Reads the first
BlockDatafrom$this->block - Converts it to an array via
$block->toArray() - Maps snake_case keys to camelCase properties using reflection
- Skips non-native typed properties (
Collection, custom objects) - Caches reflection per class for performance
Content key: section_title → Property: $sectionTitleContent key: section_description → Property: $sectionDescriptionContent key: members → Property: $membersKey Rules
Section titled “Key Rules”- Extend
JFA\FilamentCMSLivewire\Livewire\SectionComponent - No
hydrateFromContent()needed —autoHydrate()handles it - Properties are camelCase; content keys are snake_case
- Use
publicfor all properties that need Livewire serialization - Do NOT override
mount()unless absolutely necessary
Visual Editing Hook
Section titled “Visual Editing Hook”For visual editing support, add the InteractsWithVisualEditing trait and implement initializeVisualEditing():
use JFA\VeFilamentCMSLivewire\Concerns\InteractsWithVisualEditing;
class Team extends SectionComponent{ use InteractsWithVisualEditing;
public string $sectionTitle = ''; public string $sectionDescription = ''; public array $members = []; public array $cmsSourceMap = [];
protected function initializeVisualEditing(): void { $block = $this->block?->first(); if ($block !== null) { $this->cmsSourceMap = $block->getSourceMap(); } }
public function render(): \Illuminate\Contracts\View\View { return view('livewire.components.team'); }}initializeVisualEditing() is called automatically by SectionComponent::mount() after autoHydrate().
Note: Without the
InteractsWithVisualEditingtrait,renderField()is unavailable. Non-visual-editing components don’t need any trait — simply use{{ $property }}directly.
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 BC as BlockCaster
participant Section as Team Component
Page->>DB: Load page sections
DB->>Page: Sections ordered by pivot
Page->>DB: Section::resolve(pageId)
DB->>BC: Read content (BlockCaster)
BC->>Section: mount(BlockCollection)
Section->>Section: autoHydrate()
Section->>Page: Rendered HTML
Handling Missing Content
Section titled “Handling Missing Content”autoHydrate() sets default values for missing fields:
// Properties with defaults are safepublic string $sectionTitle = ''; // Gets '' if missingpublic array $members = []; // Gets [] if missingFor views that expect iterables:
@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 (auto-mapped by autoHydrate()):
// $this->members is automatically set to the array above// Result: [['name' => 'Alice', 'role' => 'Developer', ...], ...]Custom Properties (Non-Native Types)
Section titled “Custom Properties (Non-Native Types)”For properties that aren’t native types (Collection, custom objects), override initializeVisualEditing():
use Illuminate\Support\Collection;
class Services extends SectionComponent{ use InteractsWithVisualEditing;
public string $title = ''; public array $services = []; // autoHydrate handles this public Collection $serviceModels; // NOT auto-hydrated
protected function initializeVisualEditing(): void { // Load custom data manually $servicesData = $this->block?->first()?->get('services') ?? []; $ids = collect($servicesData)->pluck('service_id')->filter(); $this->serviceModels = \App\Models\Service::whereIn('id', $ids)->get();
// Set source map for visual editing $block = $this->block?->first(); if ($block !== null) { $this->cmsSourceMap = $block->getSourceMap(); } }}Multiple Block Types in One Section
Section titled “Multiple Block Types in One Section”A section can contain multiple content blocks:
class MultiBlock extends SectionComponent{ public string $title = ''; public array $paragraphs = [];
public function render(): \Illuminate\Contracts\View\View { return view('livewire.components.multi-block'); }}{{-- Get the first block of each type --}}<h1>{{ $title }}</h1>
{{-- Iterate all blocks --}}@foreach($this->block->all() as $block) <p>{{ $block->get('content') }}</p>@endforeachTroubleshooting
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 $members ?? [] fallback |
renderField() error | Missing InteractsWithVisualEditing trait | Add the trait |
| Properties empty | snake_case/camelCase mismatch | autoHydrate() maps automatically; check naming |
| TypeError on property | Non-native type | Use initializeVisualEditing() for Collection types |