sevendays/filament-page-builder
A visual page builder for Filament that lets you compose and manage dynamic pages from reusable content blocks directly in the admin panel, making flexible layouts easy to create and edit without custom code.
Requirements check first: Filament v3, Laravel 10/11, PHP 8.1+ — confirm against the composer.json of the version you actually install. The package is now at 3.0.8, so the old "pre-1.0, expect churn" assumption no longer applies: you're on a 3.x line with at least two major bumps behind it. Pin the exact version ("sevendays/filament-page-builder": "3.0.8") and read composer show -a sevendays/filament-page-builder plus the repo's releases/tags before you commit.
The 3.0.8 release notes are empty. That is itself the first thing to investigate: an empty changelog on a patch release of a small package usually means either a trivial fix or an undocumented internal change. Before upgrading an existing install, diff the two tags (git diff <previous-tag> 3.0.8 -- src/ resources/ config/) and run your test suite; do not assume a patch bump is inert.
Install and publish:
composer require sevendays/filament-page-builder:3.0.8
php artisan vendor:publish --tag="filament-page-builder-config"
php artisan vendor:publish --tag="filament-page-builder-migrations"
php artisan vendor:publish --tag="filament-page-builder-views"
php artisan migrate
If a publish tag is missing in 3.x (tags have been renamed/removed across majors), the classes are still in vendor/sevendays/filament-page-builder/ — copy what you need and note it, because you now own those files.
Register the plugin in your panel provider:
public function panel(Panel $panel): Panel
{
return $panel
->plugin(\Sevendays\FilamentPageBuilder\FilamentPageBuilderPlugin::make());
}
Confirm the exact namespace against the README bundled in vendor/ for 3.0.8 — majors are exactly where class names move. If the plugin class doesn't exist, the fallback is registering the bundled PageResource directly in the panel.
Where to look first, in order:
config/filament-page-builder.php — the block registry. This is the whole architecture: every block type is a key with a label, a Blade view, and a Filament form schema.Page model — understand the blocks column (JSON) and any slug/layout/published columns before you build on them.resources/views/vendor/filament-page-builder/ — the block views you'll be editing constantly.First use case: stand up a "Pages" resource so a marketer can assemble a landing page from a heading block, a rich-text block, and an image+text block, then render it at /p/{slug}. Do the smallest possible version first: one page, two block types, one route, one layout. You'll learn the data shape immediately and that shape drives every design decision afterwards.
Block registry as the contract. Define blocks in config with a stable key, a view, and a schema built from standard Filament components:
'blocks' => [
'hero' => [
'label' => 'Hero',
'view' => 'blocks.hero',
'form' => [
TextInput::make('heading')->required(),
Textarea::make('subheading'),
FileUpload::make('image')->image()->directory('pages'),
],
],
],
The block key is written into the page's JSON as the discriminator. Treat it as a database value, not a config value.
Class-based blocks for anything non-trivial. For blocks with logic, prefer a small class or a ->schema() closure defined in a service provider over inline config arrays — closures and objects in config/*.php break php artisan config:cache, which will bite you the first time you deploy.
Rendering loop with a safe fallback. The single most valuable pattern with any block-based builder:
@foreach ($page->blocks as $block)
@if (view()->exists('blocks.'.$block['type']))
@include('blocks.'.$block['type'], ['data' => $block['data'], 'block' => $block])
@endif
@endforeach
Without the view()->exists guard, deleting or renaming a block in config turns every page containing it into a 500. Editors will do this to you.
Layout wrapper. Keep a layout column on the page and wrap the block loop in layouts/{$page->layout}.blade.php. This lets you offer "full-width" vs "sidebar" templates without duplicating block views, and it's the cheapest way to give the package a multi-template feel it may not ship with.
Cache rendered output, not queries. Page building is read-heavy and write-rare. Render to a string and cache it:
return Cache::rememberForever("page:{$page->slug}", fn () => view('page', compact('page'))->render());
Invalidate in a model observer on saved and deleted, and also add a blocks-config hash to the cache key so config changes don't serve stale markup from removed blocks. After a 3.0.8 upgrade, flush these keys by hand — a view-level change won't invalidate markup cached under the old renderer.
Draft/preview workflow. Add published_at (or a boolean) to the pages table, filter the public route on it, and let admins preview unpublished pages through a signed temporary URL pointing at the same renderer. This is the first feature content editors ask for and it's ~15 lines.
Media handling. Pair the block schemas with Filament's Spatie Media Library plugin rather than FileUpload disk paths — you get conversions and responsive images in block views, and you avoid orphaned files (add a cleanup job keyed on media no longer referenced in any blocks JSON).
Routing. Use a constrained catch-all registered last in routes/web.php:
Route::get('/p/{page:slug}', PageController::class)
->where('slug', '[a-z0-9\-/]+')
->name('page.show');
Keep it under a prefix (/p/) rather than root-level — a root catch-all will shadow your app's routes and produce genuinely confusing 404s.
3.0.8 shipped with no notes — treat the upgrade as unverified. A patch number does not mean a safe diff in a package this size. Snapshot vendor/sevendays/filament-page-builder (or your composer.lock) before upgrading, run the admin screens and a page render as an acceptance test, and keep the previous version pinned in a branch so you can roll back in one composer require if a block schema or view signature changed silently.
Major versions are where breakage lives. Going from a 1.x-era install to 3.x means class names, config keys, and possibly the blocks JSON shape moved. If you have existing page data, export pages.blocks before upgrading and re-render every page afterwards; a changed data shape shows up as blank blocks, not as an exception.
Config closures break config:cache. Any Closure, Filament component instance, or ::class-with-arguments in config/filament-page-builder.php will fail to serialize in production (php artisan config:cache succeeds silently in some cases, then blows up in the container). Test with cached config before you deploy. Move dynamic registrations into a service provider's boot().
Block keys are immutable in practice. Renaming hero to banner in config orphans every existing page's data — the blocks simply disappear from the render loop. If you must rename, ship a migration that walks pages.blocks JSON and rewrites the type field, run it before deploying the config change. This is doubly true across a major upgrade: if 3.x renamed a default block, write that migration yourself.
Published views are a fork. Once you publish and edit the package views, package updates — including 3.0.8 — will never reach you. Either accept that, or keep published views in a separate directory and track a diff against vendor/ so you can spot what upstream changed and decide per-file whether to adopt it.
Empty registry = empty UI, no error. If the builder field renders blank in the admin, the first thing to check is whether the block registry is actually populated — a missing provider registration, a config key typo, or a renamed config key in 3.x produces a silent empty form rather than an exception.
Validation belongs inside blocks. Put ->required() on block-level fields and leave the top-level builder field nullable. A page with zero blocks is a legitimate state (a "coming soon" page) and forcing one block creates awkward UX on page creation.
Debug the raw JSON before debugging views. When a block renders wrong, dd($page->blocks) (or $page->getRawOriginal('blocks')) first. About 90% of "the builder is broken" reports are a stale cast, a missing $casts = ['blocks' => 'array'], or data written by an earlier block schema.
Sanitize any raw HTML block. If you add a RichEditor or raw HTML block, decide now whether editors are trusted. Untrusted editors plus raw HTML is stored XSS against your admin domain. Either restrict the block to a role via a policy/gate, or run output through a sanitizer in the block view.
Policy the resource. The package ships a resource that lets users create and delete pages wholesale. Add a PagePolicy and gate it — otherwise every panel user, including your read-only support role, can rewrite your marketing site.
JSON column limits. MySQL's JSON type is generous but not infinite, and a page with 200 repeater blocks can produce a payload that trips max_allowed_packet or your request body limit on save. If editors report "saving silently does nothing," check php.ini post_max_size and max_input_vars before blaming the package.
Extension points worth knowing: the published block views (override per-block markup), the config registry (add/remove block types), and the Page model (swap it in config or extend it to add accessors/observers for caching). Those three cover almost every customization you'll want; avoid patching vendor code directly. With 55 stars and no dependents you are still effectively the QA team — the difference at 3.0.8 is that upstream is evidently still moving, so prefer config- and view-level extension over forks, and expect to re-check this package at every major.
How can I help you explore Laravel packages today?