carljanzell / filament-page-builder
A drag-and-drop visual page builder for Filament, storing content as ordered typed blocks.
Package info
github.com/Darkify19/filament-page-builder
pkg:composer/carljanzell/filament-page-builder
Requires
- php: ^8.3
- filament/filament: ^5.0
Requires (Dev)
- laravel/pint: ^1.27
- livewire/livewire: ^3.6|^4.0
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v0.2.0
- v0.1.1
- v0.1.0
- dev-feat/filament-4-support
- dev-dev
- dev-filament-3
- dev-chore/post-release-followups
- dev-ci/release-workflow
- dev-chore/open-source-mit
- dev-cursor/page-builder-improvement-artifacts-1008
- dev-fix/dark-mode-editor-chrome
- dev-cursor/wordpress-style-nested-dnd-b46a
- dev-cursor/add-cloud-agent-environment-b3e7
- dev-cursor/fullscreen-live-editing-roadmap-2aef
This package is auto-updated.
Last update: 2026-09-29 08:42:00 UTC
README
A drag-and-drop visual page builder for Filament, storing page content as an ordered array of typed blocks in a single JSON column.
📖 Documentation
Status: the canvas is a nested layout editor. Palette with layout primitives (including Embed), drag into columns, a document outline, token style inspector, inline plaintext editing, undo/redo, ghost recovery and an optimistic save lock — all persisting to the same JSON the form editor uses. On a phone, Design mode is a one-panel editor (Blocks / Page / Settings). Still to come: rich text in place, an image picker, draft/publish and reusable sections. See ROADMAP.md and the improvement pack.
Why
Filament's Builder field is an excellent structured editor, but it is a form: a vertical
stack of collapsible panels. You cannot drop a block where you want it on the page, or see
the layout you are actually building. This package adds a canvas alongside it — both editing
surfaces read and write the same JSON, so neither owns the content.
Design principles
- The package owns the mechanism, the application owns the content. Blocks are classes in
your app, free to query your models and render your markup. The package never ships a
Pagemodel or a migration. - One registry, one set of components. The form, the canvas and the public renderer all resolve through the same registry, so a block cannot mean different things in each.
- Consumers never run a bundler. The canvas assets are shipped ready to serve and registered under the package's own namespace, so they never touch your application's build. This was written for hosts with no Node installed.
- Unknown block types are skipped, not fatal. Content outlives schema changes.
Installation
Requires PHP 8.3+ and Filament 5.x.
composer require carljanzell/filament-page-builder
The service provider is auto-discovered. Publish the canvas assets. If your composer.json
already runs php artisan filament:upgrade after autoload, this happens on every install:
php artisan filament:assets
Register the plugin on a panel:
use CarlJanzell\FilamentPageBuilder\FilamentPageBuilderPlugin; $panel->plugin( FilamentPageBuilderPlugin::make() ->blocks([ HeroBlock::class, RichTextBlock::class, ]) // Section, text, image, button, embed, spacer and divider ship with the package. // Register a class with the same type() to replace one. ->recordModel(\App\Models\Page::class) // stored, unused by the canvas ->blocksAttribute('blocks'), );
Apply the trait to the model that stores blocks:
use CarlJanzell\FilamentPageBuilder\Concerns\HasBlocks; class Page extends Model { use HasBlocks; }
The canvas
Extend the packaged page and bind it to your resource:
use CarlJanzell\FilamentPageBuilder\Filament\Pages\DesignPage as BaseDesignPage; class DesignPage extends BaseDesignPage { protected static string $resource = PageResource::class; }
Register it as a resource page and the canvas is available at
/admin/pages/{record}/design as a full-screen editor — Filament's sidebar and
page heading stay behind so the page itself is the workspace:
public static function getPages(): array { return [ // … 'design' => DesignPage::route('/{record}/design'), ]; }
Blocks are mutated in memory and written on an explicit save, so a drag never waits on a database round trip. Drop a Section to get columns; drag text, images and your own blocks into a column. Click a block and open the Style tab for padding, width, background and alignment — tokens, not raw CSS.
Making the canvas match your site
The package styles the builder chrome but knows nothing about how you style your blocks. Point it at a view that supplies your design tokens and block stylesheet:
FilamentPageBuilderPlugin::make() ->canvasStylesView('filament.pages.canvas-styles')
Scope that view's rules to .fpb-canvas. A bare body or h2 rule leaks out of the
canvas and restyles the builder around it.
Theming the chrome
The builder follows the panel's light and dark mode on its own. Four variables override
what it picks, set on :root for light and on html.dark .fpb for dark:
| Variable | Default | What it paints |
|---|---|---|
--fpb-editor-bg |
#f4f4f5 |
Behind the whole editor |
--fpb-panel-bg |
#fff |
The toolbar, palette and inspector |
--fpb-raised-bg |
#fff |
The selected tab and preview-width pill |
--fpb-canvas-bg / --fpb-canvas-fg |
#fff / #111827 |
The page preview itself |
The canvas keeps its light default in dark mode, because it is a preview of a public page rather than part of the admin chrome. Change the last pair if your site is dark.
Editing on the page
A block can open its own text fields for editing directly on the canvas. Declare which fields, then mark the matching element in your own markup:
use CarlJanzell\FilamentPageBuilder\Contracts\InlineEditable; use CarlJanzell\FilamentPageBuilder\Editable; class HeroBlock implements PageBlock, InlineEditable { public static function editables(): array { return [ 'heading' => Editable::text()->placeholder('Write a heading'), 'subheading' => Editable::text()->multiline(), ]; } }
<h1 @editable('heading')>{{ $data['heading'] ?? '' }}</h1>
@editable expands to editing attributes while the canvas is rendering and to nothing
anywhere else, so the public page ships the same markup without them — one component,
two contexts.
The declaration is the authority, not the markup: @editable on a field the block never
listed emits nothing, and the canvas independently refuses to write an undeclared field, a
value of the wrong kind, a richText field (until TipTap is mounted in place), or a
block the current user may not author.
Defining a block
use CarlJanzell\FilamentPageBuilder\Contracts\PageBlock; use Filament\Forms\Components\TextInput; class HeroBlock implements PageBlock { public static function type(): string { return 'hero'; } public static function label(): string { return 'Hero'; } public static function icon(): ?string { return 'heroicon-o-photo'; } public static function view(): string { return 'blocks.hero'; } public static function fileFields(): array { return ['image']; } public static function isVisible(): bool { return true; } public static function schema(): array { return [ TextInput::make('heading')->required(), ]; } }
fileFields() is explicit rather than inferred: an upload is an array in form state but a
plain path once stored, and a map of strings is indistinguishable from a repeater item.
Rendering publicly
A naive @foreach of the stored array will also print the children of a section as
top-level blocks. Use the shipped renderer, which walks the tree:
<x-page-builder::blocks :blocks="$page->blocks" />
Each column renders as a .fpb-slot inside its .fpb-section. That wrapper is what
keeps a column's blocks in that column — a section is a grid, and without it every
block becomes its own grid cell. Ship the package stylesheet on the public site, or
give .fpb-section and .fpb-slot the equivalent rules in your own theme.
Tests
composer install vendor/bin/pest
The suite boots a real Filament panel under Testbench, with its own resource, canvas page and block fixtures.
Contributing
Issues and pull requests are welcome on GitHub.
Work lands on dev, so open pull requests against dev, and run vendor/bin/pest and
composer lint before you push.
Report security problems privately, as SECURITY.md describes, rather than in a public issue.
Licence
The MIT licence. See LICENSE.