Advanced Configuration
This document covers advanced configuration patterns for the Tagixo Primix integration. These are edge-case or power-user scenarios; for day-to-day setup refer to the main plugin documentation.
1. Custom Blade Layout
By default Tagixo Primix wraps the builder UI in a standard layout that includes the Vue mount point and all required scripts. You can replace this layout via the plugin's ->layout() method.
Critical constraints that must be met by any custom layout:
- The builder mount point must carry
id="tagixo-vue". Removing or renaming this id causes Vue to fail to mount silently. - Builder JavaScript bundles are ES modules. Never append
?v=query strings to<script type="module">tags. A query string forks the module graph, causing Pinia to instantiate twice and producing phantom reactivity bugs that are extremely hard to diagnose. Use a different versioning strategy (e.g. content-hashed filenames, which Vite produces by default). - Any element in the ancestor chain of
#tagixo-vuethat carriesoverflow: hiddenwill trapposition: stickydescendants and clip dropdown panels. Setoverflow: visible(or remove the rule) on all wrappers up to and including<body>.
When to extend vs replace: if you only need to inject a <meta> tag, add analytics, or swap fonts, extend the default layout by yielding into named sections. A full layout replacement is only warranted when the structural HTML of the admin chrome (sidebar, topbar) is fundamentally different from the default.
2. Multiple Panels
You can register independent TagixoPrimixPlugin instances for separate admin panels (e.g. one for front-end pages, one for email templates). Pass a unique id to each:
TagixoPrimixPlugin::make()->id('pages'),
TagixoPrimixPlugin::make()->id('emails')->routePrefix('email-builder'),
Be aware that module registration is global — all calls to TagixoModuleRegistry::register() accumulate into a single static map regardless of which panel is active. If panels must expose different module sets, filter at render time using the hosts: / contexts: parameters documented in section 6, or gate registration behind a panel-awareness check.
Route-prefix collision warning: if two plugin instances share the same routePrefix, their routes will collide and only the last-registered set will be reachable. Always set an explicit, unique prefix for every panel beyond the first.
3. CDN Asset URL
When serving builder assets from a CDN, call ->assetBaseUrl() on the plugin instance:
TagixoPrimixPlugin::make()->assetBaseUrl(env('TAGIXO_CDN_URL', '')),
Chunk files (split by Vite, named with a content hash such as SelectProp-efUkjmQN.js) are safe to cache indefinitely — their name changes automatically when their content changes. Root entry files (builder.js, tagixo.css) must be served with a shorter TTL (or cache-busted via deployment marker) because their names do not change between patch releases but their content does.
Docker/env-var gotcha: if TAGIXO_CDN_URL is set in the container environment but not in config/, env() will return it at boot time but config() calls that happen before the service provider resolves may see null. Cache the resolved URL in a config key or resolve it directly via env() inside the plugin registration closure to avoid this race.
4. Deferred Plugin Registration
If the plugin should only be active under certain conditions (feature flags, license checks, environment), build the plugin list conditionally inside booted() rather than register():
public function boot(): void
{
$this->callAfterResolving('primix', function ($panel) {
if (MyLicenseService::hasBuilder()) {
$panel->plugin(TagixoPrimixPlugin::make());
}
});
}
Using booted() (or callAfterResolving) ensures the license service is already bound in the container. Conditional registration done inside register() may fire before service providers that the condition depends on have run.
route:cache ordering: deferred plugin registration can produce gaps in the route cache if the condition evaluates differently between the cache-generation request and subsequent requests. If you use route:cache in production, ensure the condition is deterministic and identical across all PHP processes.
5. Overriding InteractsWithVisualBuilderPrimix
The trait InteractsWithVisualBuilderPrimix wires the Primix resource to the builder save lifecycle. You may override a small set of hook methods:
| Method | Purpose | Safe to override |
|---|---|---|
onSaveSuccess() |
Called after a successful builder save | Yes |
onSaveFailure(\Throwable $e) |
Called when the save throws | Yes |
getPreviewUrl() |
Returns the front-end URL for the live-preview iframe | Yes |
Methods that must not be overridden:
| Method | Reason |
|---|---|
saveFromVue() |
Handles the raw HTTP payload; bypassing it silently drops validation and the two-phase commit |
mountInteractsWithVisualBuilderPrimix() |
Initialises state required by all other methods in the trait; overriding without calling parent:: corrupts the mount |
resolveBuilderPayload() |
Normalises the dual code-path (DefaultPageType vs BuildPage); callers assume a stable shape |
If you need to intercept payload normalisation, implement a dedicated service class and call it from onSaveSuccess() after the trait has already persisted the record.
6. Context-Scoped Module Registration
Modules can be scoped so they only appear in specific builder contexts (page builder, email builder, form builder) or on specific host applications:
ModuleRegistry::register(MyModule::class, hosts: ['tagixo-website'], contexts: ['page']);
ModuleRegistry::register(AppFormModule::class, contexts: ['form']);
Server-side filtering (the hosts: / contexts: parameters) gates which modules are shipped in the bootstrap payload. JS-side filtering (toggling visibility in the sidebar) is a UX affordance only and must never be relied upon for access control, because a determined user can replay the registration endpoint.
App-target form modules (registered with contexts: ['form']) are only visible inside the form builder canvas. They receive the form schema as their data root instead of the page payload, so their PropType definitions must reference form-scoped variables rather than global-variable tokens.
7. LiVue-Specific Wiring
When the builder is embedded inside a LiVue page (rather than a standalone Primix resource), the save event flow is:
- Builder JS calls
fetch('/tagixo/builder/save', { method: 'POST', body: payload }). InteractsWithVisualBuilderPrimix::saveFromVue()persists the record and emitstagixo:save-success(ortagixo:save-erroron failure).- The LiVue component listens for the event via
LiveDispatchand issues a notification.
<style> morph stripping: LiVue's morphing algorithm removes <style> tags injected after mount because it does not recognise them as stable DOM. This is why PrimixFormStyles injects form CSS via a <script> that writes into document.head rather than emitting a <style> tag directly — the script survives morphs because it is treated as a side-effectful node.
LiveDispatch vs Livewire dispatch(): use LiveDispatch::to($component)->event($name) when targeting a specific component from a controller or job. Bare dispatch() broadcasts to all listeners on the page and can trigger unintended handlers in complex layouts with nested Livewire trees.
8. Octane Compatibility
Octane (Swoole/FrankenPHP) keeps the PHP process alive between requests. This changes what is safe to store in static state:
Safe across requests (initialised once, immutable):
TagixoModuleRegistry— module class list, computed once at boot.- PropType definitions — stateless value objects.
Must be per-request (never store in static properties):
- Page properties resolved from the current HTTP request (slug, preview token).
- Any state derived from
Auth::user()orrequest().
The static-property trap: if a service provider stores a resolved value in a static property during boot(), that value is shared across all subsequent requests in the same worker. Symptoms include the wrong page being served to users, or admin checks passing for unauthenticated requests. Audit any class that uses static $resolved patterns.
Register cleanup callbacks via Octane::flush() for resources that must be reset between requests:
Octane::flush(fn () => MyBuilderState::reset());
9. Verbose Builder Logging
Set TAGIXO_DEBUG=true in your environment to enable verbose builder logging. This writes payload normalisation steps, module resolution, and save lifecycle events to the Laravel log.
To isolate builder logs from application logs, add a dedicated channel:
// config/logging.php
'tagixo' => [
'driver' => 'daily',
'path' => storage_path('logs/tagixo.log'),
'level' => 'debug',
'days' => 7,
],
Then set TAGIXO_LOG_CHANNEL=tagixo to route builder output to that file.
In-browser inspection: the builder bootstrap payload (module list, global variables, feature flags) is serialised into a hidden <script id="tagixo-bootstrap-data" type="application/json"> element on the builder page. You can inspect it in DevTools without network requests:
JSON.parse(document.getElementById('tagixo-bootstrap-data').textContent)
Production warning: TAGIXO_DEBUG=true logs full page payloads including all content authored by editors. Do not enable it in production environments where log files may be accessible to unauthorised parties or stored in locations with broad read permissions.