Page Builder plugin screenshot
Dark mode ready
No multilingual support
Supports v5.x

Page Builder

Community

A drag-and-drop visual page builder for Filament 5 that stores content as ordered, typed blocks in a single JSON column.

Tags: Panels Form Editor Field Forms Form Field
Supported versions:
5.x
Carl Janzell avatar Author: Carl Janzell

Package health

Beta

Automated checks of this plugin's Composer package

69 / 100
Security 43
Maintenance 100
Ecosystem 100
15 checks
  • Failed: GitHub Actions pinned to SHA — View details on Plumb
  • Skipped: GitLab CI includes pinned to SHA
  • Skipped: Open security advisories
  • Passed: Dependabot PR responsiveness — No open Dependabot PRs.
  • Skipped: Renovate MR responsiveness
  • Failed: Dependabot or Renovate configured — No dependency updater configuration found. View details on Plumb
  • Skipped: Dependency update cooldown configured
  • Passed: Provides a security policy
  • Passed: Abandoned or archived — No consulted source marks the package abandoned (packagist, github).
  • Passed: Commit and release recency — Active: last commit 0 days ago; no release date available.
  • Passed: composer.lock not committed by library — composer.lock is absent from the released dist archive.
  • Passed: Dist archive is lean
  • Passed: Current Laravel version supported — Package dependencies resolve together with current Laravel 13.0.
  • Passed: Current PHP version supported — Constraint ^8.3 supports current PHP 8.5.
  • Skipped: Current Symfony version supported
Third-party plugin. This is built by the community, not the Filament team. Filament does not review, endorse, or vet the security of plugins outside the filament/ namespace. Review the source and install at your own risk. Found malware or an unresolved security issue the author won't address? Report it .
Powered by Plumb Last scanned 12 hours ago

Documentation

A drag-and-drop visual page builder for Filament, storing page content as an ordered array of typed blocks in a single JSON column.

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 Page model 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.

The author

Carl Janzell avatar Author: Carl Janzell

Carl Janzell is a full-stack software engineer based in Los Baños, Philippines. He builds the Laravel and Filament applications behind HR operations for more than 4,000 personnel at the University of the Philippines Los Baños, with systems that exchange data over Apache Kafka. He cares most about systems thinking, event-driven architecture, and tools that make admin panels faster to build and nicer to use.

Plugins
1