Skip to content

Frontend Patterns

This guide covers the patterns and conventions used when building frontend Livewire components for the CMS.

Maintain consistency between content keys and PHP properties:

Content Key (snake_case)PHP Property (camelCase)
label$label
headline$headline
primary_cta_text$primaryCtaText
background_image$backgroundImage
members$members
// Content key: primary_cta_text
// Property: $primaryCtaText
// autoHydrate() handles the mapping automatically

All section components extend SectionComponent and let autoHydrate() handle property mapping:

namespace App\Livewire;
use JFA\FilamentCMSLivewire\Livewire\SectionComponent;
class Hero extends SectionComponent
{
public string $label = '';
public string $headline = '';
public string $primaryCtaText = '';
public string $primaryCtaUrl = '';
// No hydrateFromContent() needed!
// No mount() override needed!
// autoHydrate() maps everything automatically
public function render(): \Illuminate\Contracts\View\View
{
return view('livewire.components.hero');
}
}

For visual editing, override initializeVisualEditing():

use JFA\VeFilamentCMSLivewire\Concerns\InteractsWithVisualEditing;
class Hero extends SectionComponent
{
use InteractsWithVisualEditing;
public string $label = '';
public string $headline = '';
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.hero');
}
}

This hook is called automatically by SectionComponent::mount() after autoHydrate().

Repeaters store arrays of associative arrays:

// autoHydrate() handles this automatically
// $this->features is set from content key 'features'
@foreach($features as $feature)
<div>
<h3>{{ $feature['title'] }}</h3>
<p>{{ $feature['description'] }}</p>
</div>
@endforeach
// In initializeVisualEditing()
$this->imageUrl = $this->block?->images?->first()?->getUrl() ?? '';
// autoHydrate() maps 'background_image' to $backgroundImage automatically
public string $backgroundImage = '';
<img src="{{ asset('storage/' . $backgroundImage) }}" alt="">

Always check for content existence:

@if($headline)
<h1>{{ $headline }}</h1>
@endif
@if(!empty($features))
<div class="grid">
@foreach($features as $feature)
// ...
@endforeach
</div>
@endif

Use Tailwind CSS utility classes:

<section class="py-20 bg-gray-900 text-white">
<div class="container mx-auto px-4">
<div class="max-w-3xl mx-auto text-center">
// ...
</div>
</div>
</section>

For component-specific styles, add CSS files:

resources/css/hero.css
.hero-section {
/* component-specific styles */
}

Import in resources/css/app.css:

@import 'tailwindcss';
@import './hero.css';

Standard section structure:

<section class="py-20">
<div class="container mx-auto px-4">
{{-- Header --}}
<div class="text-center mb-12">
<span class="text-sm uppercase">{{ $label }}</span>
<h2 class="text-4xl font-bold">{{ $headline }}</h2>
</div>
{{-- Content --}}
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
// ...
</div>
{{-- CTA --}}
@if($primaryCtaText)
<div class="text-center mt-12">
<a href="{{ $primaryCtaUrl }}" class="btn-primary">
{{ $primaryCtaText }}
</a>
</div>
@endif
</div>
</section>

To use renderField(), renderImageField(), and renderRepeaterContainer() in your component, add the InteractsWithVisualEditing trait:

use JFA\VeFilamentCMSLivewire\Concerns\InteractsWithVisualEditing;
class Hero extends SectionComponent
{
use InteractsWithVisualEditing;
// No resolveFieldValue() needed — base class provides default
}

Then wrap editable fields in Blade:

{{-- Plain text --}}
<h2>{!! $this->renderField('headline', 'text') !!}</h2>
{{-- Rich text (don't escape) --}}
<div>{!! $this->renderField('body', 'rich_text', false) !!}</div>
{{-- Image --}}
<img
src="{{ $imageUrl }}"
data-cms-source="{{ $this->renderImageField('image') }}"
>
{{-- Repeater container --}}
<div data-cms-source="{{ $this->renderRepeaterContainer('items') }}">
@foreach($items as $item)
// ...
@endforeach
</div>

Note: Without this trait, renderField() is unavailable. Non-visual-editing components don’t need any trait — simply use {{ $label }} directly.

Use Laravel’s #[Computed] attribute for derived values:

use Illuminate\Support\Facades\Blade;
#[Computed]
public function hasCta(): bool
{
return !empty($this->primaryCtaText) && !empty($this->primaryCtaUrl);
}
@if($this->hasCta)
<a href="{{ $primaryCtaUrl }}">{{ $primaryCtaText }}</a>
@endif

For properties that aren’t native types, use 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
{
$servicesData = $this->block?->first()?->get('services') ?? [];
$ids = collect($servicesData)->pluck('service_id')->filter();
$this->serviceModels = \App\Models\Service::whereIn('id', $ids)->get();
$block = $this->block?->first();
if ($block !== null) {
$this->cmsSourceMap = $block->getSourceMap();
}
}
}

Listen for content updates using the listener from InteractsWithVisualEditing:

protected $listeners = [
'contentUpdated' => 'refreshFromCMS',
];

Or use the built-in listener from the trait (auto-registered when applied):

// Automatically listens for contentUpdated via InteractsWithVisualEditing
// No additional setup needed
  • Keep components focused on a single section
  • Use semantic HTML (<section>, <article>, <header>)
  • Add aria-label for accessibility
  • Use responsive classes (md:, lg:)
  • Don’t hardcode content — always pull from $this->block
  • Use fallbacks (?? '', ?? []) for all fields
  • Test with empty content to ensure graceful degradation
  • Use renderField() for visual editing support
MistakeProblemSolution
Missing fallbacksnull displayedUse ?? '' and ?? []
Wrong content keyEmpty fieldMatch snake_case in JSON
No source mapVisual editing brokenImplement initializeVisualEditing()
Escaped HTMLTags shown as textUse {!! !!} for rich text
Overriding mount()Livewire serialization breakDon’t override mount() without parent::mount()
Non-native types in propertiesTypeErrorUse initializeVisualEditing() for Collection types