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')
->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::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

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\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');
}
}
  • Extend JFA\FilamentCMSLivewire\Livewire\SectionComponent
  • Implement hydrateFromContent(SectionContent $content) (required)
  • Implement resolveFieldValue(string $field): mixed only when using visual editing (see below)
  • Do NOT override mount() unless absolutely necessary
  • Properties are camelCase; content keys are snake_case

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 InteractsWithVisualEditing trait, your component doesn’t need resolveFieldValue() at all. The trait is opt-in — only add it to components that need inline editing.

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 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

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)
// ...
@endforeach

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

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");
}
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 $team->members ?? [] fallback
renderField() errorMissing InteractsWithVisualEditing traitAdd the trait and implement resolveFieldValue()