Skip to content

Building a Section

This guide walks you through creating a complete section from the content block definition to the frontend Livewire component.

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]

Content blocks define the form fields used in the admin panel.

app/CMS/Blocks/Team.php
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::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 by BlockSchemaExtractor
  • Use ->icon() for visual identification in the builder

Add the block class to config/filament-cms-core.php:

'content_blocks' => [
'custom' => [
App\CMS\Blocks\Team::class,
// ... other blocks
],
],
app/Livewire/Team.php
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');
}
}

The base SectionComponent::mount() calls autoHydrate() which:

  1. Reads the first BlockData from $this->block
  2. Converts it to an array via $block->toArray()
  3. Maps snake_case keys to camelCase properties using reflection
  4. Skips non-native typed properties (Collection, custom objects)
  5. Caches reflection per class for performance
Content key: section_title → Property: $sectionTitle
Content key: section_description → Property: $sectionDescription
Content key: members → Property: $members
  • Extend JFA\FilamentCMSLivewire\Livewire\SectionComponent
  • No hydrateFromContent() neededautoHydrate() handles it
  • Properties are camelCase; content keys are snake_case
  • Use public for all properties that need Livewire serialization
  • Do NOT override mount() unless absolutely necessary

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 InteractsWithVisualEditing trait, renderField() is unavailable. Non-visual-editing components don’t need any trait — simply use {{ $property }} directly.

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 injects data-cms-source attributes when ve-filament-cms-livewire is installed and editing mode is active. This means your components work seamlessly with or without visual editing.

  1. Go to /adminCMS → Sections
  2. Click Create
  3. Fill in:
    • Title: “Our Team”
    • Slug: team (must match Block::make('team') and component class name)
    • Content: Add a “Team Section” block with your content
    • Status: Active
  1. Go to CMS → Pages
  2. Edit a page
  3. Go to the Sections tab
  4. Attach the “Our Team” section
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

autoHydrate() sets default values for missing fields:

// Properties with defaults are safe
public string $sectionTitle = ''; // Gets '' if missing
public array $members = []; // Gets [] if missing

For views that expect iterables:

@foreach($members ?? [] as $member)
// ...
@endforeach

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', ...], ...]

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();
}
}
}

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>
@endforeach
ProblemCauseSolution
Section not renderingWrong slugMatch slug to component class name
Empty contentWrong block typeMatch type in JSON to Block::make() name
[object Object]Nested arraysUse flat strings, not [["content" => "value"]]
foreach() errorMissing repeaterUse $members ?? [] fallback
renderField() errorMissing InteractsWithVisualEditing traitAdd the trait
Properties emptysnake_case/camelCase mismatchautoHydrate() maps automatically; check naming
TypeError on propertyNon-native typeUse initializeVisualEditing() for Collection types