Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Page Builder Laravel Package

haringsrob/filament-page-builder

A page builder for Filament that lets you create and manage dynamic pages using reusable content blocks and customizable layouts. Build flexible templates directly in the Filament admin panel and render them anywhere in your Laravel app.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and scaffold

composer require haringsrob/filament-page-builder
php artisan vendor:publish --tag="filament-page-builder-config"
php artisan vendor:publish --tag="filament-page-builder-migrations"
php artisan migrate

The package ships a pages table and a Page model by default. If you want the builder on your own model (e.g. Post, LandingPage), skip the migration and add a JSON column instead:

$table->json('content')->nullable();

Wire the model

use Haringsrob\FilamentPageBuilder\Traits\HasPageBuilder;

class Page extends Model
{
    use HasPageBuilder;

    protected $casts = [
        'content' => 'array',
    ];
}

The trait gives you the block serialization/rendering pipeline. Check the trait source for the exact method names (getPageBuilderBlocks(), renderPageBuilder()) — it's a small package and signatures have shifted between releases.

Add the builder to a Filament form — this is the first real "aha" moment:

use Haringsrob\FilamentPageBuilder\Forms\Components\PageBuilder;

public static function form(Form $form): Form
{
    return $form->schema([
        TextInput::make('title')->required(),
        PageBuilder::make('content'),
    ]);
}

Create your first block. Blocks are plain classes that declare a form schema and a view:

namespace App\PageBuilder\Blocks;

use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Haringsrob\FilamentPageBuilder\Blocks\PageBuilderBlock;

class HeroBlock extends PageBuilderBlock
{
    public static function getName(): string
    {
        return 'Hero';
    }

    public function getView(): string
    {
        return 'page-builder.blocks.hero';
    }

    public static function getFormSchema(): array
    {
        return [
            TextInput::make('heading')->required(),
            Textarea::make('subheading'),
        ];
    }
}

Register it in config/filament-page-builder.php under blocks, or per-field via PageBuilder::make('content')->blocks([...]).

Render on the frontend:

<x-page-builder :model="$page" />

That's the full loop — form field, block registry, Blade render. If you can render one block end-to-end, you understand the package.

Where to look first, in order: the config file, PageBuilderBlock base class, the HasPageBuilder trait, then the PageBuilder form component. Everything else is Filament form/Livewire plumbing you already know.

Implementation Patterns

Block-per-section, not block-per-field. The strongest pattern is one block per content section (Hero, RichText, ImageGallery, CTA, Testimonials) rather than a generic "field" block. Each block owns its own form schema and Blade view — this keeps views simple and makes the picker legible to editors.

Keep view data explicit. Pass only what the view needs by overriding the block's data method (or relying on the base class to hand the serialized array to the view). Views should never reach back into the model — treat blocks as dumb presenters:

{{-- resources/views/page-builder/blocks/hero.blade.php --}}
<section class="hero">
    <h1>{{ $data['heading'] }}</h1>
    @if($data['subheading'] ?? null)
        <p>{{ $data['subheading'] }}</p>
    @endif
</section>

Group and order blocks deliberately. Use the blocks() method on the field when a resource needs a subset, and the config file for globally available blocks. If you build a marketing site, expose marketing blocks on the LandingPage resource and editorial blocks on the Post resource — don't dump all 20 blocks on every form.

Frontend routing. For a CMS-style page, resolve slug → Page in a catch-all route and render with <x-page-builder>. For landing pages attached to other models (products, campaigns), render inline in the existing controller — the trait works on any model with the JSON column.

Reusable sub-blocks via nesting. Filament repeater/Builder fields work inside getFormSchema(). This is the escape hatch for repeatable items (slides, feature lists, FAQ rows) without registering forty block classes. Keep nesting shallow — one level is usually enough, and deep nesting makes the editor UX painful.

Translations. Multilingual sites usually keep a per-locale JSON column (content_en, content_fr) and swap the field name per Filament locale, or use a translatable cast. Either way, treat each locale's block list as independent — editors expect to reorder sections per language.

Preview and drafts. Add a status column and a preview_token to the model; the PageBuilder field reads/writes content regardless. Render the preview route with the same <x-page-builder> component against the draft record. Don't build a separate preview rendering path — divergence between preview and production is the most common page-builder bug.

Cache rendered HTML, not the blocks. Serializing blocks is cheap; rendering N views per request is not. Cache the final Blade output with a key derived from updated_at:

return Cache::remember("page.{$page->slug}.{$page->updated_at->timestamp}", 3600, fn () =>
    view('pages.show', ['page' => $page])->render()
);

Testing. Because blocks are plain classes with a declared view and schema, you can unit-test getName() / getView() and feature-test the rendered output:

$this->get('/'.$page->slug)
    ->assertSee($page->content[0]['data']['heading']);

Add a test that iterates every registered block class and asserts the view file exists — catches dead references before editors do.

Spatie Media Library integration. For image blocks, store the media ID in the block data and resolve it in the block class (getFirstMedia($data['media_id'])). Avoid storing URLs — they break on storage changes and CDN migration.

Gotchas and Tips

Block data is untyped. Everything round-trips through JSON, so numbers, booleans, and nested arrays come back as the shape you saved — but if a form field is removed from a block's schema, old data lingers silently. Add a migration or a cleanup command when you retire block fields, or editors will wonder why the old subheading still appears on the frontend.

Config caching bites. Blocks registered in config/filament-page-builder.php are cached by config:cache. After adding a block class locally, php artisan config:clear — otherwise it won't appear in the picker and you'll spend ten minutes blaming the package.

View resolution is strict. getView() must return a valid view path. If the view is missing, the builder renders nothing (or throws depending on version) — it does not fall back to a stub. Publish the package views (--tag="filament-page-builder-views") if you want to override the wrapper, but keep block views in your app.

Livewire + uploads. Image uploads inside blocks use Filament's standard FileUpload. In Filament v3 you must configure the disk and, for S3, the temporary upload directory. If previews 404 after upload, it's almost always a missing visibility: 'public' on the disk config, not the page builder.

Ordering and identity. Reordering blocks in the admin UI mutates array order in place. If you write custom code that consumes $page->content, never assume index 0 is the hero — filter by block type:

$hero = collect($page->content)->firstWhere('type', 'hero-block');

Filament version pinning. Last release was September 2024; Filament v3 iterates quickly. Pin both filament/filament and haringsrob/filament-page-builder in composer.json and test upgrades of Filament in a branch. A minor Filament bump can change form component signatures that block schemas rely on.

Extending without forking. The clean extension points are: the block base class (override getName, getIcon, getView, add helpers), the registered blocks array (swap a subclass), and the published views (wrapper markup). Avoid editing files in vendor/ — the block registry config gives you enough control to compose behavior without touching package code.

Debugging an empty render. Walk down the chain: (1) is content actually populated in the DB? (2) is the block type still in the registry? (3) does the block's view exist? (4) is the wrapper component receiving the right model? Ninety percent of "the builder isn't working" reports resolve at step 1 or 2.

Permissions. The builder is a Filament form field — it respects the resource's canEdit / policy. If editors can see blocks but not save, check the Pages policy and the JSON column's mass-assignability ($fillable includes content).

Don't over-abstract. It is tempting to build a block registry layer on top of the package's registry, a block settings panel, a versioning system. Resist. The package is intentionally thin; the fastest path to a working editorial product is one block class per section, one Blade view per block, and a straightforward cache on the rendered page.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager