Builder Pages (Filament SDK)
Builder pages integrate the Tagixo visual builder into a Filament admin panel as a standalone page or as part of a resource. This document covers everything you need to implement one correctly.
Standalone vs. Resource Builder Pages
Standalone (BuilderPage) |
Resource (BuilderAction / inline) |
|
|---|---|---|
| Entity type | Singleton — one editable record (e.g. "Home page", "Site settings") | Collection — many records managed by a Resource |
| Navigation entry | Yes, registers its own nav item | Delegated to the parent resource |
| URL | /admin/my-page |
/admin/my-resource/{record}/edit |
| When to use | There is exactly one instance of this content | Users can create/list/delete multiple instances |
| Class to extend | Ccast\TagixoFilament\Pages\BuilderPage |
Use BuilderAction in an EditRecord page |
Use a standalone BuilderPage when the content is a singleton and does not need a list view. If you find yourself hardcoding a record ID to retrieve the "one" record of a collection, that is a signal to use standalone.
BuilderPage Abstract Class
BuilderPage extends Filament's Page and wires together:
- Loading the persisted builder payload from your model
- Rendering the builder Vue application inside the page view
- Saving the builder payload back to your model on form submit
- Lifecycle hooks (
beforeSave,afterSave)
It does not handle:
- Database migrations or model creation
- Authorization beyond the
canAccess()/authorize()hooks described below - Asset publication (see Asset publication)
Three Required Methods
Every BuilderPage subclass must implement these three methods.
getRecord(): Model
protected function getRecord(): Model
Return the Eloquent model instance that holds the builder payload. The method is called once during mount() and the result is stored on $this->record. Throw a ModelNotFoundException (or any exception) if the record does not exist and the page should be inaccessible.
Gotcha: do not cache the record across requests. Because Livewire serialises the component between requests, return a fresh query result each time.
getBuilderPayload(): ?string
protected function getBuilderPayload(): ?string
Return the raw JSON string currently persisted on the model, or null if no payload has been saved yet. Return null — not an empty string — to signal "start with a blank canvas". An empty string "" is treated as invalid JSON and will throw a parse error in the builder JavaScript.
protected function getBuilderPayload(): ?string
{
return $this->record->builder_data ?: null;
}
saveBuilderPayload(string $payload): void
protected function saveBuilderPayload(string $payload): void
Persist the JSON string produced by the builder. The $payload argument is already validated JSON and has already been cleaned by the builder (removed ephemeral UI state). You do not need to json_decode / json_encode it again — write it directly.
protected function saveBuilderPayload(string $payload): void
{
$this->record->builder_data = $payload;
$this->record->save();
}
Return type is void, not bool. Communicate failures by throwing exceptions; the base class wraps the call in a try/catch and surfaces a Filament notification on error.
Artisan Scaffold
Generate a builder page stub with:
php artisan tagixo-filament:make-builder-page {name} {--model=} {--panel=}
| Option | Default | Description |
|---|---|---|
name |
(required) | Class name, e.g. HomePageBuilder |
--model |
Inferred from name | Eloquent model to bind, e.g. HomePage |
--panel |
Default panel | Panel ID to register the page into |
Generated stub (illustrative — actual output may vary by version):
<?php
namespace App\Filament\Pages;
use App\Models\HomePage;
use Ccast\TagixoFilament\Pages\BuilderPage;
use Illuminate\Database\Eloquent\Model;
class HomePageBuilder extends BuilderPage
{
protected static ?string $navigationLabel = 'Home Page';
protected static ?string $navigationIcon = 'heroicon-o-home';
protected function getRecord(): Model
{
return HomePage::firstOrFail();
}
protected function getBuilderPayload(): ?string
{
return $this->record->builder_data ?: null;
}
protected function saveBuilderPayload(string $payload): void
{
$this->record->builder_data = $payload;
$this->record->save();
}
}
Fill in $navigationLabel, $navigationIcon, and the method bodies before using the stub in production.
Page Registration
Register the page in your panel provider's pages() array:
use App\Filament\Pages\HomePageBuilder;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->pages([
HomePageBuilder::class,
]);
}
Pages listed here are automatically discovered for navigation. If you use auto-discovery (->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')), you do not need to list the class explicitly — but being explicit is recommended for builder pages to make the registration obvious.
Authorization
canAccess() — navigation and page guard
public static function canAccess(): bool
Filament calls this static method to decide whether to show the nav item and whether to allow access to the page URL. Override it to gate on a Filament permission or a Gate check:
public static function canAccess(): bool
{
return auth()->user()?->can('manage-home-page') ?? false;
}
authorize() — Livewire action guard
protected function authorize(string $ability, mixed $arguments = []): void
Filament calls authorize('update', $this->record) before processing the save action. Override when your policy uses non-standard ability names:
protected function authorize(string $ability, mixed $arguments = []): void
{
// Map 'update' → custom ability
$this->authorizeAccess();
}
Why both?
canAccess()is checked on every page load (nav render + HTTP gate).authorize()is checked on every Livewire action (form submit). A user whose role changes mid-session could bypasscanAccess()via a direct Livewire call ifauthorize()is not also hardened. Always override both if your page is sensitive.
Navigation
Control navigation appearance with static properties:
protected static ?string $navigationLabel = 'Home Page';
protected static ?string $navigationIcon = 'heroicon-o-home';
protected static ?string $navigationGroup = 'Content';
protected static ?int $navigationSort = 1;
protected static ?string $slug = 'home-page-builder'; // URL segment
To suppress the page from navigation entirely (e.g. it is opened programmatically):
protected static bool $shouldRegisterNavigation = false;
You can also override the navigation item dynamically:
public static function getNavigationItems(): array
{
if (! static::canAccess()) {
return [];
}
return parent::getNavigationItems();
}
Complete Working Example
Migration and Model
// database/migrations/2024_01_01_000000_create_home_pages_table.php
Schema::create('home_pages', function (Blueprint $table) {
$table->id();
$table->longText('builder_data')->nullable();
$table->timestamps();
});
// app/Models/HomePage.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class HomePage extends Model
{
protected $fillable = ['builder_data'];
}
Builder Page
// app/Filament/Pages/HomePageBuilder.php
namespace App\Filament\Pages;
use App\Models\HomePage;
use Ccast\TagixoFilament\Pages\BuilderPage;
use Illuminate\Database\Eloquent\Model;
class HomePageBuilder extends BuilderPage
{
protected static ?string $navigationLabel = 'Home Page';
protected static ?string $navigationIcon = 'heroicon-o-home';
protected static ?string $navigationGroup = 'Content';
protected static ?int $navigationSort = 10;
/**
* mount() ordering gotcha: $this->record is set by the parent mount()
* via getRecord(). Do NOT call getRecord() yourself before calling
* parent::mount() — the parent sets $this->record first, then runs
* fillForm(), which calls getBuilderPayload(). If you need to do work
* after the record is loaded, use afterMount() or override mount() and
* call parent::mount() at the top.
*/
public function mount(): void
{
parent::mount(); // sets $this->record, fills the builder form
// Safe to access $this->record here
$this->getSubHeading(); // example of post-mount work
}
protected function getRecord(): Model
{
// firstOrCreate ensures the page always exists
return HomePage::firstOrCreate([], []);
}
protected function getBuilderPayload(): ?string
{
return $this->record->builder_data ?: null;
}
protected function saveBuilderPayload(string $payload): void
{
$this->record->update(['builder_data' => $payload]);
}
public static function canAccess(): bool
{
return auth()->user()?->hasRole('admin') ?? false;
}
}
Panel Provider Registration
// app/Providers/AdminPanelProvider.php
use App\Filament\Pages\HomePageBuilder;
public function panel(Panel $panel): Panel
{
return $panel
->id('admin')
->path('admin')
->pages([
HomePageBuilder::class,
]);
}
Asset Publication
Builder pages require the Tagixo JavaScript and CSS assets to be published. Publish from the core package (ccast/tagixo), not from the Filament integration package:
php artisan vendor:publish --tag=tagixo-assets --force
Publishing from
tagixo-filament(if such a tag exists) only publishes Filament-specific views. The builder canvas assets always live in the core package.
Run this command whenever you upgrade ccast/tagixo and commit the updated files in public/vendor/tagixo/ to version control. The production Dockerfile runs composer install --no-scripts, so assets must be pre-built and committed.
Extension Contract Summary
All overridable members of BuilderPage, with signatures:
| Member | Type | Required | Description |
|---|---|---|---|
getRecord(): Model |
method | Yes | Returns the model instance to bind |
getBuilderPayload(): ?string |
method | Yes | Returns persisted JSON or null |
saveBuilderPayload(string $payload): void |
method | Yes | Persists the JSON string |
canAccess(): bool |
static method | No | Gate for nav + page access |
authorize(string $ability, mixed $arguments): void |
method | No | Gate for Livewire actions |
beforeSave(): void |
method | No | Hook called before payload is saved |
afterSave(): void |
method | No | Hook called after payload is saved |
getTitle(): string |
method | No | Page heading in the admin chrome |
$navigationLabel |
static property | No | Text in the sidebar nav |
$navigationIcon |
static property | No | Icon in the sidebar nav |
$navigationGroup |
static property | No | Group heading in the sidebar nav |
$navigationSort |
static property | No | Sort order within group |
$slug |
static property | No | URL segment (defaults to kebab class name) |
$shouldRegisterNavigation |
static property | No | Set false to hide from nav |
See Also
- Filament SDK Integration — overview of the full Filament integration
- Standalone Page Builder Integration — embedding the builder outside Filament
- Storage Model and Data Shape — what the JSON payload looks like
- Production Checklist and Troubleshooting — asset publication, Octane cache clearing