Contexts, Layout Variants, and Builder Types
Tagixo is not a single-purpose page builder — it powers page editing, form authoring, email design, PDF generation, and slider composition from one unified core. The mechanism that partitions these surfaces is the context system. Every builder session runs under a named context that governs which modules are visible, which PropTypes are surfaced, how the canvas is structured, and which feature panels the editor exposes. Understanding contexts is mandatory when you are shipping a plugin with custom modules or building a purpose-specific builder on top of Tagixo.
The context registry
At boot time, the core calls ContextRegistry::register() for each built-in context. The registry is a plain keyed store; each entry maps a context string to a config array describing its capabilities. You can inspect the resolved registry via app(\Ccast\Tagixo\Core\ContextRegistry::class)->all() in Tinker, or read the source directly at src/Core/ContextRegistry.php in the ccast/tagixo package.
Config shape
Each context entry accepts the following keys:
| Key | Type | Description |
|---|---|---|
label |
string |
Human-readable name shown in admin UI selectors |
layout_variant |
string |
Canvas structure variant — default or carousel |
module_filter |
string|null |
Optional class name of a ContextModuleFilter implementation |
proptype_filter |
string|null |
Optional class name of a ContextPropTypeFilter implementation |
feature_policy |
string|null |
Optional class name of a ContextFeaturePolicy implementation |
canvas_defaults |
array |
Default payload shape injected when a new document is created in this context |
preview_device |
string |
Default preview mode: desktop, tablet, or mobile |
allowed_section_types |
array|null |
Restricts the section types (e.g. ['default', 'full-width']) or null for all |
Only label is strictly required. All other keys have safe defaults.
How the registry is queried
When a builder request arrives (via the bootstrap endpoint), Tagixo resolves the context string from the request payload, looks it up in the registry, and composes the bootstrap response from the registry config:
// Internal flow — simplified
$contextConfig = $registry->get($request->context); // e.g. 'mail'
$modules = ComponentRegistry::forContext($request->context);
$propTypes = PropTypeRegistry::forContext($request->context);
$featureFlags = ContextFeaturePolicy::resolve($contextConfig['feature_policy'] ?? null);
return BootstrapResponse::make($modules, $propTypes, $featureFlags, $contextConfig);
The bootstrap response is serialized to JSON and consumed by the Vue frontend. There is no client-side context branching — everything is resolved server-side before the editor mounts.
Built-in contexts
Tagixo ships four contexts out of the box:
| Context | Builder surface | Layout variant | Notes |
|---|---|---|---|
page |
Page / layout builder | default |
All modules available unless explicitly restricted |
form |
Form builder | default |
Only form field and form wrapper modules available |
mail |
Email template builder | default |
Restricted module set; no interactive or animation modules |
pdf |
PDF template builder | default |
Further restrictions; no web-only features |
A fifth identifier, partial, is used internally for global blocks and reusable sections. It behaves like page but the canvas is stripped of header/footer layout wrapping.
The carousel layout variant
The Slider Builder is not a separate context. It runs under context=page with layout_variant=carousel. The distinction matters because:
- All page modules remain available (they appear inside slide content areas).
- The canvas renders a horizontally scrollable slide track instead of a vertical page column.
- Section types are restricted — only slide-compatible section shapes are permitted.
- The payload
canvas_defaultsdiffer: the bootstrap seeds an initial slide structure instead of a blank page body. - Transition, timing, and pagination props become top-level canvas props that have no equivalent in a standard page context.
If you are writing a module that should appear inside slides, you do not need to do anything special — any module available in page is available in carousel. If you need to render differently when inside a slide (e.g. suppress a fixed-position overlay), check the layout_variant in your Blade template via the render context helpers:
// Inside a module's Blade view
@if ($renderContext->layoutVariant() === 'carousel')
{{-- slide-specific markup --}}
@endif
Module availability per context
Each module class declares its allowed contexts through ModuleDefinition::contexts() inside the definition() method. The registry automatically filters modules at bootstrap time — modules absent from the filter do not appear in the editor palette, are not included in the availableComponents payload, and cannot be dropped onto the canvas.
use Ccast\Tagixo\Core\Abstracts\AbstractModule;
use Ccast\Tagixo\Core\ModuleDefinition;
class AnimatedCounterModule extends AbstractModule
{
public function definition(): ModuleDefinition
{
return ModuleDefinition::make()
->label('Animated Counter')
->icon('heroicon-o-arrow-trending-up')
->category('interactive')
->contexts(['page']); // page only — intentionally excludes mail and pdf
}
// ...
}
Cross-context modules list every context they support:
->contexts(['page', 'mail', 'pdf'])
Modules that omit contexts() default to ['page']. This means a newly scaffolded module is page-only unless you explicitly broaden it. Do not broaden it blindly — verify that the module's CSS, JavaScript dependencies, and rendering output are safe in each additional context you add.
Gotcha: the
pagecontext and thecarousellayout variant share the same module pool. Adding a module topageautomatically makes it droppable inside slides. If your module relies on page-level scroll events or viewport calculations that break inside the slide track, restrict it or add alayout_variantguard in the Blade view.
Runtime module filtering
For cases where static context arrays are not expressive enough, you can implement ContextModuleFilter:
use Ccast\Tagixo\Core\Contracts\ContextModuleFilter;
use Ccast\Tagixo\Core\Registries\ComponentRegistry;
class NewsletterModuleFilter implements ContextModuleFilter
{
public function filter(array $modules, string $context): array
{
// Remove modules whose type IDs are in our exclusion list.
$excluded = config('acme-newsletter.excluded_modules', []);
return array_filter(
$modules,
fn ($m) => ! in_array($m::getTypeId(), $excluded, true)
);
}
}
Register the filter on your custom context via config/tagixo.php, or override a built-in context's module filter by publishing the config and editing the contexts array entry for mail, pdf, etc.
PropType filtering per context
PropTypes follow the same filtering pattern. If a PropType makes no sense in a context — for example, an AnimationPropType in mail — you can hide it without removing it from the registry globally.
Implement ContextPropTypeFilter:
use Ccast\Tagixo\Core\Contracts\ContextPropTypeFilter;
class MailPropTypeFilter implements ContextPropTypeFilter
{
/** Keys of PropTypes to suppress in this context. */
private const EXCLUDED = ['animation', 'box_shadow', 'filter', 'transform'];
public function filter(array $propTypes, string $context): array
{
return array_filter(
$propTypes,
fn ($pt) => ! in_array($pt->key(), self::EXCLUDED, true)
);
}
}
Register on a built-in context by publishing config/tagixo.php and setting the proptype_filter key for the mail entry in contexts.
This removes the matching design panels from every module's property drawer when the editor is running under the mail context. Individual PropType instances are not destroyed — they remain registered — so modules that check hasPropType() do not need to change.
Builder UI feature gating per context
The module palette and PropType tabs control what content editors can add and style. Per-context UI feature gating (hiding entire panels, tabs, or toolbar controls based on context) is not yet available as a public API.
To restrict the builder UI for a custom context, use a ContextModuleFilter to remove unsupported module types from the palette, and a ContextPropTypeFilter to hide inapplicable design panels (e.g. animation, box_shadow for mail contexts).
Registering a custom context
Custom contexts are added via config/tagixo.php. Add a new entry to the contexts array:
// config/tagixo.php
'contexts' => [
// Built-in contexts (page, form, mail, pdf) are already present.
'email-newsletter' => [
'label' => 'Email Newsletter',
'icon' => 'heroicon-o-envelope',
'description' => 'Build email newsletter templates',
'enabled' => true,
],
],
There is no runtime Tagixo::registerContext() method — the contexts config key is the only registration point.
Once the key exists in config, the context string can be passed to the builder embed URL:
/tagixo/builder/embed?type=pages&id=1&context=email-newsletter
Or wired to a BuilderPage / BuildPage class via the context() method:
// Primix
class Pages\EditNewsletter extends BuildPage
{
protected function context(): string { return 'email-newsletter'; }
}
// Filament
class Pages\EditNewsletter extends BuilderPage
{
protected function context(): string { return 'email-newsletter'; }
}
Layout variants and canvas structure
The layout_variant config key controls how the Vue canvas is scaffolded. There are two values:
| Variant | Canvas structure | Used by |
|---|---|---|
default |
Vertical page column with section stacking, header/footer layout regions | Page, Form, Mail, PDF builders |
carousel |
Horizontal slide track with per-slide section areas and transition controls | Slider Builder |
There is no API to add custom layout variants from a plugin — variants are hard-coded canvas modes in the Vue frontend. If you need fundamentally different canvas geometry, you must ship a custom Vue entry point. In practice, nearly all custom contexts run on default.
The Form builder is a default variant but its canvas is structurally different from the Page builder because the module set is limited to form fields and form wrappers. The canvas hierarchy is identical (section → row → column → module) but form fields declare subElements that map to field label, input, helper text, and error state — stylable parts the page canvas has no concept of. This is a module-level concern, not a canvas-level one; the variant is default in both cases.
Builder type map from a plugin-dev perspective
Understanding which code path handles each builder type is essential when debugging bootstrap failures or implementing context-aware rendering.
| Builder | Context | Variant | Bootstrap param | Renderer class |
|---|---|---|---|---|
| Page builder | page |
default |
?context=page |
PageRenderer |
| Layout builder | page |
default |
?context=page&type=layout |
PageRenderer |
| Slider builder | page |
carousel |
?context=page&layout_variant=carousel |
PageRenderer (slide mode) |
| Form builder | form |
default |
?context=form |
FormRenderer |
| Mail builder | mail |
default |
?context=mail |
MailRenderer |
| PDF builder | pdf |
default |
?context=pdf |
PdfRenderer |
| Custom context | your-context |
default |
?context=your-context |
Resolved by RendererFactory |
Custom contexts fall back to PageRenderer by default, which is a safe default for most layout-based contexts. To use a custom renderer, extend PageRenderer and point config('tagixo.renderers.page') to your class in config/tagixo.php:
// config/tagixo.php
'renderers' => [
'page' => \Acme\Newsletter\NewsletterRenderer::class,
],
Note: Layout builder and Page builder share the same context string (
page) and renderer. The?type=layoutparameter tells the admin page to bind the content record to aLayoutmodel instead of aPagemodel — it is a resource-level distinction, not a context-level one.
Building a plugin that introduces a new context
A plugin that adds a new context needs to coordinate three things: the context registration, the module set, and (optionally) a custom renderer. The following outline covers a complete "documentation page" context that restricts editors to a minimal content-focused module palette.
1. Register the context in the plugin class
namespace Acme\DocBuilder;
use Ccast\Tagixo\Facades\Tagixo;
use Ccast\Tagixo\TagixoPluginBase;
class DocBuilderPlugin extends TagixoPluginBase
{
public function getId(): string
{
return 'acme/doc-builder';
}
public function boot(Tagixo $tagixo): void
{
// Register context-specific modules
$tagixo->registerModules([
\Acme\DocBuilder\Modules\CodeBlockModule::class,
\Acme\DocBuilder\Modules\CalloutBoxModule::class,
\Acme\DocBuilder\Modules\ApiTableModule::class,
]);
}
}
2. Restrict modules to the new context
namespace Acme\DocBuilder\Modules;
use Ccast\Tagixo\Core\Abstracts\AbstractModule;
use Ccast\Tagixo\Core\ModuleDefinition;
class CodeBlockModule extends AbstractModule
{
public function definition(): ModuleDefinition
{
return ModuleDefinition::make()
->label('Code Block')
->icon('heroicon-o-code-bracket')
->category('developer')
->contexts(['docs']); // Only appears in the docs context
}
}
3. Implement DocsModuleFilter to suppress unwanted built-ins
namespace Acme\DocBuilder;
use Ccast\Tagixo\Core\Contracts\ContextModuleFilter;
class DocsModuleFilter implements ContextModuleFilter
{
private const ALLOWED = [
'text', 'heading', 'image', 'divider', 'spacer', 'table',
'code',
// Acme custom
'docs-code-block', 'docs-callout-box', 'docs-api-table',
];
public function filter(array $modules, string $context): array
{
return array_filter(
$modules,
fn ($m) => in_array($m::getTypeId(), self::ALLOWED, true)
);
}
}
4. Wire the admin builder page
Create a Primix BuildPage subclass that targets your new context and bind it to the Primix resource that manages documentation pages:
namespace Acme\DocBuilder\Pages;
use Ccast\Tagixo\Primix\BuildPage;
class EditDocPage extends BuildPage
{
protected function context(): string
{
return 'docs';
}
}
Register EditDocPage as the edit page of your DocPageResource.
JSON stability and context migrations
Context strings are stored in the tgx_pages.context column (or equivalent on custom models). If you rename a context string after content has been saved, existing records will return a 404 on the bootstrap endpoint because the registry lookup will fail.
When renaming a context:
- Add an alias in the registry before removing the old key.
- Run a migration to update the stored context strings.
- Remove the alias after deployment.
To rename a context safely:
- Add the new key to
config/tagixo.php → contextswhile keeping the old key present. - Run a database migration updating the stored context strings.
- Remove the old key from config after deployment.
There is no runtime alias API — config is the only registration point.
Stability rule: treat context strings with the same discipline as module type IDs. Both are persisted in the database and changing them without a migration breaks saved content.
See also
- Module System and Catalog — full module reference and context column
- Sub-elements and States — how form field sub-elements differ from page module sub-elements
- PropTypes System — PropType contract and registration
- Tagixo Plugin — packaging custom contexts in a reusable Composer plugin
- Rendering Pipeline — how
PageRenderer,MailRenderer, andPdfRendererconsume context - Custom Page Modules — step-by-step guide for authoring modules
- Scaffold Commands — generator reference for modules, PropTypes, and plugins