Core Integration

Builder API and Endpoints

Understand bootstrap, render, stylesheet, preview, global variables, and model metadata endpoints.

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:savePOST /tagixo/manage/{type}/{id}/save
  • tagixo:save-global-variablesPOST /tagixo/builder/global-variables
  • tagixo:structure-changedPOST /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 availableComponents and propTypeRegistry directly.


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_html column 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=1 and 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 if dompdf/dompdf is 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