CMS voor home pagina

CMS voor home pagina

Voor cltopmaat.nl heb ik een CMS ontwikkeld, wat ervoor zorgt dat hun Home pagina beheert kan worden vanuit het admin paneel. De klant wilde zijn home pagina zelf in kunnen vullen, omdat de data hiervan veel veranderde. Om dit te realiseren heb ik gekozen voor een light weight CMS met Filament, zodat we wel Laravel kunnen blijven gebruiken en de klant toch zijn pagina kan aanpassen.

Front-end met Vue.js en Inertia

Eerst heb ik een Inertia-pagina Home.vue aangemaakt waar we de componenten van de homepagina laden (DynamicComponent.vue). Laravel haalt alle HomeComponent.php-records op uit de database en geeft deze door aan de Inertia-pagina:

defineProps<{
    homeComponents: Array<App.Data.HomeComponentData>
}>()
</script>

<template>
    <HomeLayout>
        <Head title="Home" />
        <main>
            <template
                v-for="component in homeComponents"
                :key="component.type">
                <DynamicComponent
                    :component="component">
            </template>
        </main>
    </HomeLayout>
</template>

HomeComponentData bevat de dynamische data die nodig is om het component te laten werken. In dit project had ik een aantal vaste variabelen die altijd aanwezig zijn bij elk component:

export type HomeComponentData = {
    id: number;
    type: string;
    title: string | null;
    subtitle: string | null;
    description: string | null;
    cta_text: string | null;
    cta_url: string | null;
    image_url: string | null;
    video_url: string | null;
    cta_type: any | any;
    content: { [key: string]: any } | null;
};

Het content-object is dynamisch en kan willekeurige data bevatten. DynamicComponent.vue bevat de koppeling van de string representatie van het component naar het daadwerkelijke component:

const props = defineProps<{
    component: App.Data.HomeComponentData
}>()

const componentMap = {
    hero: Hero,
    features: Features,
    pricing: Pricing,
} as const

const Component = componentMap[props.component.type as keyof typeof componentMap]
</script>

<template>
    <Component
        v-if="Component"
        :is="Component"
        v-bind="component" />
</template>

Vervolgens bevat elk component zijn eigen configuratie, bijvoorbeeld Hero.vue:

const props = defineProps({
    title: {
        type: String,
        default: "",
    },
    subtitle: {
        type: [String, null],
        default: null,
    },
    content: {
        type: Object,
        default: () => ({
            section_label: "",
            primary_cta: { text: "", url: "#" },
            secondary_cta: { text: "", url: "#" },
            images: [],
        }),
    },
})

Naast een titel en ondertitel heeft het diverse afbeeldingen in een carrousel en een primaire en secundaire CTA-knop.

Back-end/CMS met Laravel en Filament

Alle records worden opgeslagen in het HomeComponent.php-model:

class HomeComponent extends Model
{
    /** @use HasFactory<HomeComponentFactory> */
    use HasFactory;

    protected $fillable = [
        'type',
        'title',
        'subtitle',
        'description',
        'cta_text',
        'cta_url',
        'cta_type',
        'content',
        'order',
        'is_active',
    ];

    protected $casts = [
        'content' => 'array',
        'is_active' => 'boolean',
        'order' => 'integer',
    ];

    public function newEloquentBuilder($query): HomeComponentQueryBuilder
    {
        return new HomeComponentQueryBuilder($query);
    }
}

Om elk component bewerkbaar te maken vanuit Filament, moeten we de database vullen met een seeder:

$components = [
            [
                'type' => 'hero_redesign',
                'title' => 'Ontwerp, ontvang advies en bestel CLT voor jouw project',
                'subtitle' => 'Upload jouw ontwerp, ontvang een beoordeling en bestel direct. Het complete CLT-proces in één platform.',
                'description' => null,
                'content' => [
                    'section_label' => 'De toekomst van houtbouw',
                    'primary_cta' => [
                        'text' => 'Ontvang gratis advies',
                        'url' => route('review-packages'),
                    ],
                    'secondary_cta' => [
                        'text' => 'Ontdek hoe het werkt',
                        'url' => route('platform'),
                    ],
                    'images' => [
                        ['image_path' => 'home-components/clt_example.webp'],
                    ],
                ],
                'order' => 0,
            ],
];

Nu kunnen we in HomeComponentResource.php conditionele invoervelden plaatsen. Bijvoorbeeld voor het hero-component hebben we primary_cta_text en primary_cta_url:

Forms\Components\Repeater::make('content.images')
    ->label(__('home_component.carousel_images'))
    ->schema([
        Forms\Components\FileUpload::make('image_path')
            ->label(__('Image'))
            ->image()
            ->required()
            ->preserveFilenames()
            ->directory('home-components'),
    ])
    ->minItems(1)
    ->columnSpanFull()
    ->visible(fn (Forms\Get $get) => $get('type') === 'hero_redesign'),

Met ->visible(fn (Forms\Get $get) => $get('type') === 'hero') zorgen we ervoor dat deze invoervelden alleen zichtbaar zijn voor het hero-component.