carljanzell/filament-page-builder
A page builder for Filament that lets you assemble pages from reusable blocks. Define custom block types, drag and drop them into flexible layouts, and edit content straight from the admin panel. A practical way to give editors structured control over Laravel site pages.
Treat this as a Filament form field plus a JSON storage convention, not a full CMS. The package gives editors a drag-and-drop block editor; you still own the block library, rendering, routing, validation, and caching.
v0.3.1 line. Filament 4.12.6+ and 5.x use v0.4.x and require PHP 8.3+. Pin an exact version in composer.json; this is a low-adoption package (14 stars, 0 dependents), so avoid floating constraints.$table->json('content')->nullable();
Cast it on the model:
protected $casts = [
'content' => 'array',
];
src/ for the class implementing Filament\Forms\Components\Field if the README is thin. Configure the allowed block types there rather than letting editors insert arbitrary types.@foreach ($page->content ?? [] as $block)
@includeIf("page-builder.blocks.{$block['type']}", [
'data' => $block['data'] ?? [],
'block' => $block,
])
@endforeach
First use case: a simple marketing page with hero, text, and image blocks. Build those three Blade partials first, add them to the allowed block list, create one page in Filament, and render it through a normal Laravel route. That exercises the full loop—schema, editor, JSON shape, renderer—before you add columns, nesting, or previews.
Where to look first: the README, src/ for the field and renderer classes, any config file for block registration, and the package tests/examples for the exact JSON shape. Confirm the shape with a real saved record before writing renderers.
Keep a single block registry. Define each block type once: its label, icon, allowed fields, validation rules, and Blade partial. Pass that allow-list into the builder field per resource. This prevents editors from inserting a block that has no renderer and gives you one place to add new block types.
Render through partials, not a giant match. A recursive renderer pays off because the package supports columns and nested content (the v0.4.2 release notes mention columns and the form editor deleting or duplicating content inside them). Use a partial that renders one block and recurses into child columns:
{{-- page-builder/block.blade.php --}}
@php($type = $block['type'] ?? null)
@if ($type === 'columns')
<div class="grid">
@foreach ($block['data']['columns'] ?? [] as $column)
@foreach ($column as $child)
@include('page-builder.block', ['block' => $child])
@endforeach
@endforeach
</div>
@else
@includeIf("page-builder.blocks.{$type}", ['data' => $block['data'] ?? []])
@endif
Guard unknown types with @includeIf plus a log warning so one bad block does not break an entire page.
Separate page concerns from block content. Keep slug, title, SEO fields, status, and publish dates as normal model columns. Store only ordered block content in the JSON column. That keeps queries, routing, and publishing simple and avoids turning the JSON into an unqueryable mini-database.
Use a draft/published pattern. A common approach is content for the working copy and published_content for the live copy, or a single JSON column plus published_at. Duplicate-page actions are easy because the content is one JSON value—but remember to deep-copy the array, not share references.
Validate block data server-side. Filament will validate field-level input, but add a rule or service that checks each block has a known type and expected keys before saving. If you expose a frontend preview endpoint, validate there too; never render arbitrary type values as a partial path without an allow-list.
Cache rendered pages deliberately. Rendering dozens of blocks on every request is usually fine, but for high-traffic pages cache the final HTML and invalidate on model save. Do not cache the builder editor output itself; editors need fresh state after saves.
Localization: either use one JSON column per locale (content_en, content_es) or store locale-keyed content under a stable shape. Pick one early; retrofitting locale into an already-populated JSON payload is painful.
Media handling: upload images through Filament's FileUpload inside block data and store paths, not full URLs. Add a cleanup job or observer to delete orphaned media when blocks are removed, because JSON payloads do not give you database-level cascade deletion.
Testing pattern: write one Livewire/Filament form test that saves a page with a hero block and one nested columns block, then assert the stored JSON is a list in the expected order and that the frontend route returns the rendered partial output. This is the highest-value test for a package with sparse public documentation.
v0.4.x on Filament 3 will not work; use v0.3.1. On Filament 4.12.6+/5.x, require PHP 8.3+.->spa() and columns.tinker or a DB client. Package docs may not describe nesting, wrapper keys, or block IDs precisely.{!! !!} user-authored block content unless your editor roles are fully trusted.saved and deleted events. Editors will otherwise report “my change didn't appear” after a successful save.How can I help you explore Laravel packages today?