Frontend Patterns
This guide covers the patterns and conventions used when building frontend Livewire components for the CMS.
Property Naming
Section titled “Property Naming”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 ?? '';Mount Pattern
Section titled “Mount Pattern”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 — BindingResolutionExceptionpublic function mount(SectionContent $content): voidHydration Pattern
Section titled “Hydration Pattern”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 ?? '';}Visual Editing Hook
Section titled “Visual Editing Hook”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().
Repeater Handling
Section titled “Repeater Handling”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>@endforeachImage Handling
Section titled “Image Handling”From Spatie Media Library
Section titled “From Spatie Media Library”protected function hydrateFromContent(SectionContent $content): void{ $this->imageUrl = $content->images->first()?->getUrl() ?? '';}From FileUpload Block
Section titled “From FileUpload Block”protected function hydrateFromContent(SectionContent $content): void{ $this->backgroundImage = $content->hero->background_image ?? '';}<img src="{{ asset('storage/' . $backgroundImage) }}" alt="">Conditional Rendering
Section titled “Conditional Rendering”Always check for content existence:
@if($headline) <h1>{{ $headline }}</h1>@endif
@if(!empty($features)) <div class="grid"> @foreach($features as $feature) // ... @endforeach </div>@endifCSS Classes
Section titled “CSS Classes”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:
.hero-section { /* component-specific styles */}Import in resources/css/app.css:
@import 'tailwindcss';@import './hero.css';Component Layout
Section titled “Component Layout”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>Visual Editing Integration
Section titled “Visual Editing Integration”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.
Computed Properties
Section titled “Computed Properties”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>@endifEvents
Section titled “Events”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 neededBest Practices
Section titled “Best Practices”- Keep components focused on a single section
- Use semantic HTML (
<section>,<article>,<header>) - Add
aria-labelfor 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 theInteractsWithVisualEditingtrait for visual editing
Common Mistakes
Section titled “Common Mistakes”| Mistake | Problem | Solution |
|---|---|---|
mount(SectionContent $content) | BindingResolutionException | Use mount($content = []) |
| Missing fallbacks | null displayed | Use ?? '' and ?? [] |
| Wrong content key | Empty field | Match snake_case in JSON |
| No source map | Visual editing broken | Implement initializeVisualEditing() and getCmsSourceMap() |
| Escaped HTML | Tags shown as text | Use {!! !!} for rich text |
Missing resolveFieldValue() | Error on renderField() | Add InteractsWithVisualEditing trait and implement resolveFieldValue() |