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
$this->primaryCtaText = $content->hero->primary_cta_text ?? '';

Always use mount($content = []) with manual normalization:

public function mount($content = []): void
{
$sectionContent = $content instanceof SectionContent
? $content
: new SectionContent($content);
$hero = $sectionContent->hero;
$this->label = $hero->label ?? '';
$this->headline = $hero->headline ?? '';
}

Never type-hint SectionContent directly:

// WRONG — BindingResolutionException
public function mount(SectionContent $content): void

When extending SectionComponent, implement hydrateFromContent():

protected function hydrateFromContent(SectionContent $content): void
{
$hero = $content->hero;
$this->label = $hero->label ?? '';
$this->headline = $hero->headline ?? '';
$this->primaryCtaText = $hero->primary_cta_text ?? '';
$this->primaryCtaUrl = $hero->primary_cta_url ?? '';
}

For visual editing, override initializeVisualEditing() and implement getCmsSourceMap():

public array $cmsSourceMap = [];
protected function initializeVisualEditing(): void
{
$sourceMap = $this->sectionContent->hero->getSourceMap();
if ($sourceMap !== null) {
$this->cmsSourceMap = $sourceMap;
}
}
protected function getCmsSourceMap(): array
{
return $this->cmsSourceMap;
}

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

Repeaters store arrays of associative arrays:

// In hydrateFromContent
$this->features = $content->hero->features ?? [];
// In Blade
@foreach($features as $feature)
<div>
<h3>{{ $feature['title'] }}</h3>
<p>{{ $feature['description'] }}</p>
</div>
@endforeach
protected function hydrateFromContent(SectionContent $content): void
{
$this->imageUrl = $content->images->first()?->getUrl() ?? '';
}
protected function hydrateFromContent(SectionContent $content): void
{
$this->backgroundImage = $content->hero->background_image ?? '';
}
<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 from ve-filament-cms-livewire:

use JFA\VeFilamentCMSLivewire\Concerns\InteractsWithVisualEditing;
class Team extends SectionComponent
{
use InteractsWithVisualEditing;
// ... also implement resolveFieldValue()
}

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

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 $content
  • Use fallbacks (?? '', ?? []) for all fields
  • Test with empty content to ensure graceful degradation
  • Implement resolveFieldValue() only when using the InteractsWithVisualEditing trait for visual editing
MistakeProblemSolution
mount(SectionContent $content)BindingResolutionExceptionUse mount($content = [])
Missing fallbacksnull displayedUse ?? '' and ?? []
Wrong content keyEmpty fieldMatch snake_case in JSON
No source mapVisual editing brokenImplement initializeVisualEditing() and getCmsSourceMap()
Escaped HTMLTags shown as textUse {!! !!} for rich text
Missing resolveFieldValue()Error on renderField()Add InteractsWithVisualEditing trait and implement resolveFieldValue()