The file looks correct. Here is the full documentation content:
Builder API and Endpoints
Tagixo registers all builder routes through its service provider under the /tagixo prefix. There is exactly one HTML route (the builder mount) and everything else returns JSON. Your standalone controllers call these endpoints, consume their payloads, and implement the complementary save and dataPayload logic in your own code.
All routes require the middleware stack defined in config/tagixo.php under route_middleware. The default is ['web', 'auth']. Override per your auth guard — Sanctum, tenant guard, or a custom pipeline — but keep web unless you know exactly why you are removing it: signed preview URLs, CSRF verification, and session-aware preview bypasses all depend on standard Laravel web behaviour.
// config/tagixo.php
'route_middleware' => ['web', 'auth'],
CSRF tokens must be present in every mutating request. The builder JS reads the meta tag emitted by @csrf:
<meta name="csrf-token" content="{{ csrf_token() }}">
Builder Mount
GET /tagixo/builder/embed
The only endpoint that returns HTML. Renders the tagixo::builder.embed Blade, which mounts the Vue SPA. Dispatched by BuilderEmbedController, which resolves your BuilderTypeContract handler from the registry, calls editorPayload($record), and injects the resulting URLs into data-* attributes on the mount div.
Query parameters
| Parameter | Required | Description |
|---|---|---|
type |
Yes | Registry key (pages, mails, pdfs, or any key you registered). |
id |
Yes | Primary key of the record to edit. |
back |
No | URL injected as data-back-url. Omit to hide the back button. |
Mount div emitted by the Blade
<div
id="tagixo-vue"
data-page-id="{{ $id }}"
data-context="{{ $context }}"
data-layout-variant="{{ $layoutVariant }}"
data-data-url="{{ $dataUrl }}"
data-save-url="{{ $saveUrl }}"
data-config-url="{{ $configUrl }}"
data-back-url="{{ $backUrl }}"
></div>
The bundled JS wires three internal events to the appropriate endpoints:
tagixo:save→POST /tagixo/manage/{type}/{id}/savetagixo:save-global-variables→POST /tagixo/builder/global-variablestagixo:structure-changed→POST /tagixo/builder/stylesheet
Error responses
| Status | Cause |
|---|---|
404 |
type not registered or id not found. |
422 |
Missing required query parameter. |
Link to this URL from your admin list view. The plugin owns the editor surface; your app owns the surrounding chrome.
Bootstrap
GET /tagixo/builder/bootstrap
GET /tagixo/builder/config (alias)
Called once by the Vue SPA immediately after mount. Returns every piece of metadata the frontend needs to hydrate: module catalog, PropType registry, available fonts, global variables, registered data models, context metadata, and endpoint URLs. Treat this as the canonical source of truth for the frontend — never ship a parallel hardcoded registry.
Query parameters
| Parameter | Required | Description |
|---|---|---|
context |
Yes | Builder context: page, form, mail, pdf, or partial. |
layout_variant |
No | Layout variant key, e.g. default or carousel. Defaults to default. |
bound_model |
No | Registry key of the model currently bound to this edit session (e.g. post). Scopes attribute pickers in the sidebar. |
structure |
No | Current structure JSON. When provided, the server can return structure-aware metadata. |
Example request
GET /tagixo/builder/bootstrap?context=page&layout_variant=default&bound_model=post
Full annotated response shape
{
// ── Context ───────────────────────────────────────────────────────────────
"context": "page",
"layoutVariant": "default",
// ── Module catalog ────────────────────────────────────────────────────────
"registry": {
"components": [
{
"key": "heading",
"label": "Heading",
"group": "content",
"icon": "heroicon-h1",
"slots": ["default"],
"meta": {}
}
// …more modules
],
"propTypes": {
"ColorPropType": {
"component": "ColorPicker",
"defaultValue": null
},
"SpacingPropType": {
"component": "SpacingControl",
"defaultValue": null
}
// …keyed by class short name
}
},
// ── Compatibility aliases (same data, different keys) ─────────────────────
"availableComponents": [ /* same as registry.components */ ],
"propTypeRegistry": { /* same as registry.propTypes */ },
// ── Resources ─────────────────────────────────────────────────────────────
"resources": {
"icons": ["heroicon-check", "heroicon-arrow-right" /* … */],
"fonts": [
{ "family": "Inter", "weights": [400, 600, 700] }
],
"translations": { "save": "Save", "preview": "Preview" /* … */ },
"globalVariables": [
{ "key": "brand_primary_color", "value": "#3b82f6", "type": "color" }
],
"dataModels": {
"post": {
"label": "Blog Post",
"attributes": ["id", "title", "slug", "excerpt", "published_at"],
"relationships": ["author", "category"]
}
}
},
// ── Canvas metadata ───────────────────────────────────────────────────────
"canvas": {
"maxWidth": 1280,
"defaultPadding": 24
},
// ── Endpoint map ──────────────────────────────────────────────────────────
"endpoints": {
"save": "/tagixo/manage/pages/42/save",
"data": "/tagixo/manage/pages/42/data",
"render": "/tagixo/builder/render",
"stylesheet": "/tagixo/builder/stylesheet",
"previewUrl": "/tagixo/builder/preview-url",
"globalVariables": "/tagixo/builder/global-variables",
"models": "/tagixo/builder/models"
},
// ── Availability flags ────────────────────────────────────────────────────
"availableFonts": [ /* same as resources.fonts */ ]
}
Key field reference
| Field | Type | Notes |
|---|---|---|
registry.components |
array | Only modules visible in this context and layout variant. |
registry.propTypes |
object | Keyed by class short name. The Vue SPA maps these to sidebar control components. |
resources.globalVariables |
array | Current snapshot. The editor polls GET /tagixo/builder/global-variables separately for live updates. |
resources.dataModels |
object | Registered models with attribute allowlist. Only models registered via Tagixo::registerModels() appear. |
endpoints |
object | Pre-computed URL map the SPA uses for all subsequent calls. |
Never hardcode a parallel component registry in your frontend. If the bootstrap payload and your frontend diverge, modules will silently fail to render. Consume
availableComponentsandpropTypeRegistrydirectly.
Data (per-record)
GET /tagixo/manage/{type}/{id}/data
Called by the Vue SPA after the builder mounts to load the persisted structure for the record. Dispatched by the generic CRUD layer to your BuilderTypeContract::dataPayload($record) implementation. You control the full response shape.
Minimal dataPayload implementation
public function dataPayload(Model $record): array
{
return [
'structure' => $record->content ?? ['body' => [], 'components' => []],
'bodyProps' => [],
'context' => 'page',
'layoutVariant' => 'default',
'globalVariables' => [],
];
}
Annotated response shape
{
"structure": {
"body": {
"background": { "color": "#ffffff" },
"spacing": { "padding": { "top": "0", "right": "0", "bottom": "0", "left": "0" } }
},
"components": [
{
"id": "sec_1",
"type": "section",
"parent_id": null,
"order": 0,
"props": {}
}
]
},
"bodyProps": {},
"context": "page",
"layoutVariant": "default",
"globalVariables": []
}
The structure field must always contain a body object and a components array. Never return a null structure — return an empty scaffold ({ "body": {}, "components": [] }) for new records. The Vue SPA treats a missing or null structure as an unrecoverable bootstrap failure.
Save
POST /tagixo/manage/{type}/{id}/save
Receives the builder payload after each explicit save action. Dispatched to your BuilderTypeContract::save($record, $request) implementation. You own validation, persistence, and re-render.
Request body
{
"structure": {
"body": { "background": { "color": "#f5f5f5" } },
"components": [
{
"id": "sec_1",
"type": "section",
"parent_id": null,
"order": 0,
"props": { "spacing": { "padding": { "top": "40", "bottom": "40" } } }
}
]
},
"context": "page"
}
| Field | Type | Required | Notes |
|---|---|---|---|
structure |
object | Yes | Full builder payload. Contains body and components. |
context |
string | Yes | Same context string used at bootstrap. |
Minimal save implementation
use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\Model;
use Ccast\Tagixo\Renderers\PageRenderer;
public function save(Model $record, Request $request): array
{
$structure = $request->validate([
'structure' => ['required', 'array'],
'structure.body' => ['required', 'array'],
'structure.components' => ['required', 'array'],
'context' => ['required', 'string'],
])['structure'];
$renderer = app(PageRenderer::class);
$rendered = $renderer->renderFromJson($structure);
$record->update([
'content' => $structure,
'rendered_html' => $rendered['html'],
'css' => $rendered['css'],
]);
return ['success' => true];
}
Success response
{ "success": true }
Always re-render and persist HTML/CSS artifacts on save, not on public request. Serving the cached
rendered_htmlcolumn at runtime avoids re-rendering on every page view. If you skip the artifact step, either pre-render at request time (viable for low-traffic sites) or accept that the public page will be stale until the next explicit save.
Global Variables
GET /tagixo/builder/global-variables
POST /tagixo/builder/global-variables
PUT /tagixo/builder/global-variables
Returns the full set of global variable tokens available to the editor's variable picker. These are color, text, number, font, and typography tokens that editors can bind to any compatible prop — centralizing brand values so a single change propagates everywhere.
GET response shape
[
{ "key": "brand_primary_color", "value": "#3b82f6", "type": "color" },
{ "key": "brand_secondary_color", "value": "#10b981", "type": "color" },
{ "key": "layout_max_width", "value": "1280", "type": "number" },
{ "key": "content_cta_label", "value": "Get started", "type": "text" },
{ "key": "body_typography", "value": { "fontFamily": "Inter", "fontSize": "16px" }, "type": "typography" }
]
Variable type reference
| Type | Value shape | CSS output |
|---|---|---|
color |
hex or rgba string | --tgx-{key}: {value} on :root |
number |
numeric string (no unit) | --tgx-{key}: {value}px on :root |
text |
plain string | Not emitted as CSS; used by token interpolation |
font |
font family string | --tgx-{key}: {value} on :root |
typography |
object with fontFamily, fontSize, color, etc. |
Full typography block under :where(.vb-body) |
heading_typography |
object with per-level settings | Per-heading-level rules under :where(.vb-body) |
Reserved keys
The keys body_typography and heading_typography are reserved by the renderer. Do not register custom variables under these keys — doing so silently overwrites the site-wide typography cascade.
POST / PUT — save global variables
{
"variables": [
{ "key": "brand_primary_color", "value": "#ff6600", "type": "color" },
{ "key": "layout_max_width", "value": "1440", "type": "number" }
]
}
The endpoint upserts by key. The full array replaces the current variable set — include all variables you want to keep, not just the changed ones.
Model Metadata
GET /tagixo/builder/models
GET /tagixo/builder/models/{key}/attributes
Returns the schema of registered Eloquent models. The editor uses this to populate field-mapping controls, token pickers, and dynamic content bindings (for example a list module bound to post that renders live records).
Register models before the endpoint has anything to return:
// AppServiceProvider or a dedicated TagixoServiceProvider
use Ccast\Tagixo\Facades\Tagixo;
public function boot(): void
{
Tagixo::registerModels([
'post' => [
'class' => App\Models\Post::class,
'label' => 'Blog Post',
'relationships' => ['author', 'category'],
'only' => ['id', 'title', 'slug', 'excerpt', 'published_at'],
],
'product' => App\Models\Product::class, // shorthand; key = 'product'
]);
}
GET /tagixo/builder/models — response shape
{
"post": {
"key": "post",
"label": "Blog Post",
"class": "App\\Models\\Post"
},
"product": {
"key": "product",
"label": "Product",
"class": "App\\Models\\Product"
}
}
GET /tagixo/builder/models/{key}/attributes — response shape
{
"key": "post",
"label": "Blog Post",
"attributes": [
{ "name": "id", "type": "integer" },
{ "name": "title", "type": "string" },
{ "name": "slug", "type": "string" },
{ "name": "excerpt", "type": "string" },
{ "name": "published_at", "type": "datetime" }
],
"relationships": [
{ "name": "author", "type": "BelongsTo", "related": "App\\Models\\User" },
{ "name": "category", "type": "BelongsTo", "related": "App\\Models\\Category" }
]
}
Only attributes listed in only (or not listed in except) are returned. Do not expose models with sensitive attribute sets — treat model registration as a product boundary, not just a developer shortcut.
Pass bound_model on the bootstrap request when you know the editing session is scoped to a specific model type. The bootstrap payload will include that model's attribute schema in context:
GET /tagixo/builder/bootstrap?context=page&bound_model=post
Preview
POST /tagixo/builder/preview-url
GET /tagixo/builder/preview-mail/{id}
GET /tagixo/builder/preview-pdf/{id}
Preview works through server-issued signed URLs. The editor never receives a raw payload to render client-side — it requests a signed URL, opens it in a new tab, and lets the server render the final output through the same pipeline as production.
Issuing a signed preview URL
Request
POST /tagixo/builder/preview-url
{
"context": "page",
"entity_id": 42
}
| Field | Required | Notes |
|---|---|---|
context |
Yes | page, mail, or pdf. |
entity_id |
Yes | Primary key of the record. |
Response
{
"preview_url": "https://yourapp.com/my-page-slug?_preview=1&expires=1752067200&signature=abc123",
"expires_in": 300
}
For context=page, the URL points at the entity's public slug with _preview=1, expires, and signature appended. For context=mail or context=pdf, the URL points at /tagixo/builder/preview-mail/{id} or /tagixo/builder/preview-pdf/{id} respectively.
Signed URL lifetime
// config/tagixo.php
'preview' => [
'url_ttl_seconds' => 300, // 5 minutes; adjust to your workflow
],
Honouring the preview bypass in your public controller
For context=page, your PublicPageController must allow signed preview requests to bypass the published() gate. Call Page::isAuthorizedPreviewRequest($request) to check all three conditions — valid signature, _preview=1 query param, and authenticated session — before deciding whether to serve an unpublished page:
use Ccast\Tagixo\Models\Page;
public function show(Request $request, string $slug): Response
{
$query = Page::where('slug', $slug);
if (! Page::isAuthorizedPreviewRequest($request)) {
$query->published(); // only published pages for regular visitors
}
$page = $query->firstOrFail();
$html = app(\Ccast\Tagixo\Renderers\PageRenderer::class)
->renderWithLayout($page);
return response($html);
}
All three conditions must hold for preview bypass. Valid signature alone is not enough — the request must also carry
_preview=1and belong to an authenticated session. If your middleware strips the session or the user is not logged in when opening the preview tab, the bypass fails and the page returns 404. Test preview from the same browser session you used to open the editor.
Mail and PDF preview endpoints
GET /tagixo/builder/preview-mail/{id}— HTML email body. Useful for checking rendered layout and inline CSS before a send.GET /tagixo/builder/preview-pdf/{id}— print-ready HTML (or binary PDF ifdompdf/dompdfis installed). Opens inline in the browser.
Both require a valid signed URL issued by POST /tagixo/builder/preview-url.
Render
POST /tagixo/builder/render
Returns a full HTML+CSS+fonts render of a structure. Called by the builder canvas to update the live preview pane. You can also call this endpoint from your own code to generate render artifacts on demand.
Request body
{
"structure": {
"body": {},
"components": [
{
"id": "sec_1",
"type": "section",
"parent_id": null,
"order": 0,
"props": {}
}
]
},
"context": "page",
"layout_variant": "default",
"entity_type": "page",
"entity_id": "42"
}
| Field | Required | Notes |
|---|---|---|
structure |
Yes | Builder payload with body and components. |
context |
Yes | Selects the renderer branch (page, form, mail, pdf). |
layout_variant |
No | default or carousel. Defaults to default. |
entity_type |
No | Pass when the renderer needs entity-aware resolution. |
entity_id |
No | Pass together with entity_type. |
Response
{
"html": "<div class=\"vb-section\" ...>...</div>",
"css": ".vb-section { ... }",
"fonts": "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/...\">"
}
Render branches
| Context + variant | Renderer |
|---|---|
context=form |
Form preview renderer |
context=page, layout_variant=carousel |
Slider preview renderer |
| Everything else | PageRenderer::renderFromJson() |
Always pass context and layout_variant explicitly. Guessing at the edge of your system causes the wrong renderer to fire, producing broken output even when the structure is valid.
Stylesheet
POST /tagixo/builder/stylesheet
Returns regenerated CSS only, without re-rendering HTML. The builder fires this on every prop change (colour, spacing, font) to update styles live without a full canvas refresh.
Request body — identical to the render endpoint, but only structure and context are required:
{
"structure": { "body": {}, "components": [] },
"context": "page"
}
Response
{
"css": ".vb-section { padding-top: 40px; } ..."
}
Use this endpoint in your own tooling when you need a CSS artifact from a structure change without the HTML overhead — for example, regenerating only the CSS column after an admin edits a global variable.
Full endpoint reference
| Method | Path | Purpose |
|---|---|---|
GET |
/tagixo/builder/embed |
Builder mount — the only HTML route. |
GET |
/tagixo/builder/bootstrap |
Module catalog, PropTypes, fonts, global variables, endpoint map. |
GET |
/tagixo/builder/config |
Alias for /bootstrap. |
GET |
/tagixo/manage/{type}/{id}/data |
Per-record structure, dispatched to dataPayload(). |
POST |
/tagixo/manage/{type}/{id}/save |
Persist structure, dispatched to save(). |
GET |
/tagixo/builder/global-variables |
All global variable tokens. |
POST PUT |
/tagixo/builder/global-variables |
Upsert global variable tokens. |
GET |
/tagixo/builder/models |
All registered model schemas. |
GET |
/tagixo/builder/models/{key}/attributes |
Attribute + relationship schema for one model. |
POST |
/tagixo/builder/render |
Full HTML+CSS render. |
POST |
/tagixo/builder/stylesheet |
CSS-only render. |
POST |
/tagixo/builder/preview-url |
Issue signed preview URL. |
GET |
/tagixo/builder/preview-mail/{id} |
Inline rendered mail (signed). |
GET |
/tagixo/builder/preview-pdf/{id} |
Inline rendered PDF (signed). |
GET |
/tagixo/manage/{type} |
Index records for a registered type. |
POST |
/tagixo/manage/{type} |
Create a new record. |
DELETE |
/tagixo/manage/{type}/{id} |
Delete a record. |
See also
- Standalone Page Builder Integration —
BuilderTypeContractimplementation, builder mount URL, and theeditorPayload/dataPayloaddistinction. - Rendering and Preview Workflow —
PageRenderer::renderFromJson()internals, render branches, and public page assembly withrenderWithLayout(). - Storage Model and Data Shape — canonical
body + componentspayload, node contract, and save strategy. - Data Models and Global Variables —
Tagixo::registerModels(), taxonomy registration, and global variable governance. - Laravel Bootstrap and Configuration —
config/tagixo.phpreference, route middleware, and context configuration.