Advanced Configuration
This page covers advanced configuration patterns for the Tagixo Filament SDK integration. For basic setup, see Filament SDK Integration.
1. Custom Builder Blade Layout
By default the builder page is wrapped in tagixo-filament::layout, a minimal Blade layout that includes the Livewire script stack and the Tagixo asset bundle. You can publish and override it:
php artisan vendor:publish --tag=tagixo-filament-views
This copies the view to resources/views/vendor/tagixo-filament/layout.blade.php. The layout must keep all three Livewire directives or the builder will silently fail to boot:
@livewireStyles
{{-- your custom head content --}}
@livewireScripts
@stack('scripts')
Do not remove @stack('scripts') — the builder Vue app is pushed onto that stack at render time.
To point a specific BuilderPage subclass at a custom layout instead of the global one, override the $layout property:
class MyCustomBuilderPage extends \Ccast\TagixoFilament\Pages\BuilderPage
{
protected string $layout = 'my-panel::builder-layout';
}
Panel-level layout overrides only affect pages that inherit from that subclass; other builder pages continue to use the vendor default.
2. Multiple Filament Panels
If your application registers more than one Filament panel, register the plugin only on the panels that should expose the builder. The plugin carries its own middleware, asset injection, and route prefix — loading it into an unintended panel will expose builder endpoints under that panel's auth guard.
// AdminPanelProvider.php — expose builder here
->plugins([
\Ccast\TagixoFilament\TagixoFilamentPlugin::make(),
])
// ApiPanelProvider.php — omit the plugin entirely
->plugins([
// no TagixoFilamentPlugin
])
Each plugin instance is independent. You may configure them differently (different asset URLs, different debug modes) for staging vs. production panels within the same codebase:
TagixoFilamentPlugin::make()
->assetUrl(config('app.env') === 'production'
? 'https://cdn.example.com/tagixo'
: null
)
3. Customizing Asset Paths
By default the plugin serves its JS/CSS bundle from public/vendor/tagixo/. If you host static assets on a CDN or under a non-standard public path, use ->assetUrl():
TagixoFilamentPlugin::make()
->assetUrl('https://cdn.example.com/assets/tagixo')
The value is prepended to all asset filenames (builder.js, builder.css, etc.). It must not have a trailing slash.
For multi-tenant setups where the CDN subdomain is request-dependent, pass a lazy closure:
->assetUrl(fn () => 'https://' . tenant()->cdn_domain . '/tagixo')
The closure is resolved once per request, after the service container is fully booted, so it can read the current tenant from the container or from a helper. Do not perform database queries inside this closure on high-traffic pages — cache the resolved value in a request-scoped singleton if needed.
4. Deferred Service Provider Registration
The plugin's service provider is not deferred by default because it registers view composers and route macros that must be available early. If your application has many packages and boot time is a concern, you can implement deferred registration for the parts you control:
class AppServiceProvider extends ServiceProvider implements DeferrableProvider
{
public function provides(): array
{
return [MyBuilderExtension::class];
}
public function register(): void
{
$this->app->singleton(MyBuilderExtension::class, fn () => new MyBuilderExtension());
}
}
This defers only your own bindings. The Tagixo plugin itself (TagixoFilamentServiceProvider) cannot be made deferrable without modifying the vendor package — see the upstream repo for that configuration.
Deferred providers are resolved when their provided classes are first requested from the container. In practice this means the first request that touches the builder will still pay the full boot cost; subsequent requests in an Octane context reuse the resolved instance. For most applications the complexity is not worth the marginal gain.
5. Overriding Builder Save/Load Trait Methods
BuilderPage composes InteractsWithVisualBuilder, which provides loadStructure() and saveStructure(). Override these to integrate with custom persistence logic:
| Scenario | Override |
|---|---|
Save to a custom model instead of tgx_pages |
saveStructure(array $payload): void |
| Load from an external API | loadStructure(): array |
| Post-process payload before render | preparePayloadForRender(array $payload): array |
| Validate structure before saving | validateStructure(array $payload): void |
protected function loadStructure(): array
{
return MyRemoteApi::fetchPage($this->record->remote_id);
}
protected function saveStructure(array $payload): void
{
MyRemoteApi::updatePage($this->record->remote_id, $payload);
$this->record->touch(); // keep local updated_at in sync
}
Public API boundary warning: only loadStructure, saveStructure, preparePayloadForRender, and validateStructure are considered stable override points. Methods prefixed with boot, mount, or handle are internal and may change between minor releases. If you need to hook into those, open a feature request upstream rather than overriding them.
6. Custom Module Registration Scoped to Filament
To register modules only when the Tagixo Filament plugin is active — for example, to avoid loading heavy PHP dependencies in contexts where the builder is not used — use the static whenActive helper:
// In a service provider boot() method
TagixoFilamentPlugin::whenActive(function () {
Tagixo::registerModule(MyHeavyModule::class);
});
For per-panel module sets, use whenActiveForPanel:
TagixoFilamentPlugin::whenActiveForPanel('admin', function () {
Tagixo::registerModule(AdminOnlyModule::class);
});
TagixoFilamentPlugin::whenActiveForPanel('marketing', function () {
Tagixo::registerModule(MarketingOnlyModule::class);
});
Both callbacks are executed during the panel boot sequence, after the plugin is registered but before the first request is handled. They are not deferred — if the panel is booted, the callback runs regardless of whether the builder page is actually visited.
7. Octane Compatibility
Laravel Octane keeps the application in memory between requests. This means any state accumulated in singletons during one request is visible to the next. The Tagixo plugin is designed to be Octane-safe for its own singletons, but custom extensions must follow the same rules.
Safe patterns:
- Stateless services bound with
$app->bind()(new instance per request) - Read-only configuration resolved in
register()and never mutated - Services that flush their own state via
RequestHandledorOctane\Events\RequestReceived
Risky patterns:
- A singleton that accumulates per-request data (e.g., a collector that appends to an array)
- Caching the current user or tenant in a static property on a service
To flush request-scoped state between Octane requests, listen to the framework event:
use Laravel\Octane\Events\RequestHandled;
$this->app['events']->listen(RequestHandled::class, function () {
app(MyBuilderStateCollector::class)->flush();
});
OPcache and asset trap: Octane caches PHP files in OPcache. After running vendor:publish --tag=tagixo-assets, the new asset files are on disk but old PHP-side asset manifests may be cached. Run php artisan octane:reload after every asset publish to flush OPcache and pick up the new manifest. Failing to do so results in the browser receiving cache-busted URLs that point to non-existent files.
8. Debug Mode
Enable debug mode on the plugin to surface builder-internal errors in the browser console and in Filament's notification stack:
TagixoFilamentPlugin::make()->debug()
Alternatively, set the environment variable and the plugin will pick it up automatically:
TAGIXO_DEBUG=true
In debug mode the plugin exposes:
- The raw JSON payload sent to and received from the builder Vue app
- Server-side render errors as visible overlay messages instead of silent fallbacks
- Asset URL resolution logs (useful when debugging CDN path issues)
- Module registration order and any duplicate-key warnings
Production warning: never ship TAGIXO_DEBUG=true or ->debug() on a production panel. The raw payload contains the full page structure including any unpublished draft content, and the overlay messages may leak internal model IDs or file paths to authenticated-but-unprivileged users.
9. Integrating with Filament Shield
Filament Shield generates Spatie Permission roles and policies for Filament resources and pages. To protect the builder page:
php artisan shield:generate --all
This creates a view_builder permission (derived from the page class name). To require it, add HasPageShield to your BuilderPage subclass:
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
class MyBuilderPage extends \Ccast\TagixoFilament\Pages\BuilderPage
{
use HasPageShield;
}
If you need a custom permission slug (for example, because multiple builder pages share the same guard):
public static function getPermissionPrefixes(): array
{
return ['view', 'edit'];
}
protected function getShieldRedirectPath(): string
{
return route('filament.admin.pages.dashboard');
}
For role assignment in a seeder (common in staging/CI environments):
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
$adminRole = Role::firstOrCreate(['name' => 'super_admin', 'guard_name' => 'web']);
$permission = Permission::firstOrCreate(['name' => 'view_builder', 'guard_name' => 'web']);
$adminRole->givePermissionTo($permission);
$user = \App\Models\User::where('email', config('app.admin_email'))->first();
$user?->assignRole($adminRole);
Shield's super_admin role bypasses all permission checks by default — verify your AuthServiceProvider or Shield config if you need strict enforcement even for super admins.