Standalone Page Builder Integration
This page covers the integration model where your Laravel application uses Tagixo directly, without depending on a panel SDK.
What "standalone" actually means
In a standalone setup:
- Tagixo provides the builder engine, renderers, module registry, PropType registry, and headless API.
- Your app provides the host UI, authorization, persistence, publishing logic, and routing.
Tagixo does not need to own your business entities.
Source of truth
Your application should treat the builder JSON as the source of truth.
At minimum, each editable entity should store:
- raw builder structure
- publishing metadata
- optional cached render artifacts
Typical columns:
contentJSONrendered_htmlLONGTEXT nullablecssLONGTEXT nullablestatuspublished_attitleslug
Example model:
class MarketingPage extends Model
{
protected $casts = [
'content' => 'array',
'published_at' => 'datetime',
];
}
Canonical structure you should persist
The canonical Tagixo structure is:
{
"body": {
"background": { "color": "#ffffff" }
},
"components": [
{
"id": "sec_1",
"type": "section",
"parent_id": null,
"order": 0,
"props": {
"spacing": {
"padding": {
"top": "24",
"right": "24",
"bottom": "24",
"left": "24"
}
}
}
}
]
}
Important:
bodyis a plain props object, not a node object.componentsis usually stored in flat form.- hierarchy is reconstructed from
parent_idandorder.
Rendering example
use Ccast\Tagixo\Renderers\PageRenderer;
$renderer = app(PageRenderer::class);
$result = $renderer->renderFromJson([
'body' => [],
'components' => $components,
]);
$html = $result['html'];
$css = $result['css'];
Save pipeline
Your save pipeline should be explicit:
- Receive structure from the builder.
- Validate that
bodyis an array andcomponentsis an array. - Validate each node shape.
- Persist raw JSON.
- Re-render HTML/CSS.
- Persist render artifacts if your delivery strategy uses caching.
Example service-layer flow:
public function savePage(MarketingPage $page, array $structure): void
{
$renderer = app(PageRenderer::class);
$rendered = $renderer->renderFromJson($structure);
$page->update([
'content' => $structure,
'rendered_html' => $rendered['html'],
'css' => $rendered['css'],
]);
}
Skip the boilerplate: unified CRUD endpoints
Standalone projects often hand-roll repetitive index/edit/data/save controllers — one set per entity type. Tagixo ships a generic, type-driven CRUD that eliminates that boilerplate.
The flow:
- Implement
Ccast\Tagixo\Contracts\BuilderTypeContractfor each entity type (pages,mails,forms,sliders, …). The contract has methods likedataPayload($id)(returns the JSON consumed by the Vue builder) andsave($id, array $structure). - Register it with
BuilderTypeRegistry::register('pages', YourPageType::class)— typically inside a service provider. - The package exposes a generic route set serving any registered type:
| Method | URI | Name | Returns |
|---|---|---|---|
| GET | /tagixo/manage/{type} |
tagixo.manage.index |
{data: [records...], type, meta} |
| POST | /tagixo/manage/{type} |
tagixo.manage.store |
{data: record, type} (201) |
| GET | /tagixo/manage/{type}/{id}/data |
tagixo.manage.data |
handler's dataPayload() |
| POST | /tagixo/manage/{type}/{id}/save |
tagixo.manage.save |
handler's save() result |
| DELETE | /tagixo/manage/{type}/{id} |
tagixo.manage.destroy |
{success: true} |
These five routes are JSON-only. To open the visual builder for a record, point the user at /tagixo/builder/embed?type={type}&id={id} — see the "Builder mount" section below. Tagixo is REST-first; there is exactly one HTML route in the entire package.
The package also auto-registers a DefaultPageType, DefaultMailType, and DefaultPdfType (gated by tagixo.enable_default_types), so a fresh standalone install has working pages, mails, pdfs endpoints with zero code.
When to use it (and when not to):
- ✅ Use for standalone Laravel apps with no admin framework — saves a controller per entity type.
- ❌ Skip if you use the Filament SDK or Primix SDK — those provide admin CRUD via their own Resource model (
/admin/pages/*). The routes coexist without interfering, but typically you don't reference/tagixo/manage/*directly when an SDK is wired. - The
dataandsavepayload shapes are handler-defined — seeBuilderTypeContract::dataPayload()/save()for your contract.
If your entity types match the standard editor/builder shape, this is the path of least resistance. If you have unusual workflow (multi-step publishing, custom approval gates), the hand-rolled controller in the next section still applies.
Public rendering strategy
Two paths — choose one:
Mode A — let the plugin serve /{path?}
Set tagixo.frontend.enabled = true (default) and tagixo.frontend.auto_routes = true (opt-in). The plugin registers a Laravel fallback route that runs through PublicPageController:
- it activates only when no other route matches —
/login,/dashboard, any admin path, your custom endpoints all keep priority - it walks the
Pageslug hierarchy (parent_id chain) to resolve the request path - returns
Page::renderFull()— full<!DOCTYPE html>with head, meta, OG, robots, canonical, fonts, body rendered_html+cssare reused from the row when present (no re-render per request)
Gotcha — Laravel's default
Route::get('/')welcome view: a brand-new Laravel skeleton ships withRoute::get('/', fn () => view('welcome'))inroutes/web.php. Regular routes beatfallbackroutes, so that line captures/before the plugin can serve ahome_slugpage. If you want a published page namedhometo render at the root URL, delete the welcome route (or replace it with your own logic). AnyRoute::get('/', …)you keep wins over the fallback — that's intentional, so you can mix a custom landing with plugin-served slugs.
Mode B — render from your own routes
Leave frontend.enabled = false (or auto_routes = false) and serve pages yourself. Page::renderFull(?string $layoutView = null): string is the single entry point — it composes header + body + footer + CSS + font links. You decide caching, headers, tenant scoping.
Example public controller
public function show(string $slug)
{
$page = MarketingPage::query()
->where('slug', $slug)
->where('status', 'published')
->firstOrFail();
return view('pages.show', [
'page' => $page,
'content' => $page->rendered_html,
'css' => $page->css,
]);
}
Injecting custom scripts and styles
The default frontend Blade (tagixo::frontend.page, used by Page::renderFull() and the Mode A fallback) exposes two @stack hooks for third-party scripts, analytics pixels, CDN libraries, or extra <meta> tags — so you do not need to publish and edit the Blade for the common case.
| Stack | Position in the page | Typical use |
|---|---|---|
tagixo-head |
just before </head> |
analytics loaders, preconnect, additional meta tags, extra <link rel="stylesheet"> |
tagixo-body |
just before </body> |
end-of-body scripts, pixel beacons, deferred libraries |
Push to them from any Blade view, layout, or partial:
@push('tagixo-head')
<link rel="preconnect" href="https://cdn.example.com">
<script defer src="https://cdn.example.com/lib.js"></script>
@endpush
@push('tagixo-body')
<script>
// analytics pageview, pixel fire, etc.
</script>
@endpush
From a service provider, register a view composer that pushes the same content on every render:
use Illuminate\Support\Facades\View;
public function boot(): void
{
View::composer('tagixo::frontend.page', function () {
View::startPush('tagixo-head');
echo '<script defer src="https://cdn.example.com/lib.js"></script>';
View::stopPush();
});
}
A common site-wide pattern is to keep the snippets in your own partials (e.g. resources/views/partials/analytics-head.blade.php) and @include them inside the @push block — that way the markup stays editable without touching PHP.
If you need more than injection — reorder the markup, change <html> attributes, wrap the body in a global container — publish the Blade and edit it directly:
php artisan vendor:publish --tag=tagixo-views
The frontend layout lands at resources/views/vendor/tagixo/frontend/page.blade.php. Keep the four variables it consumes ($page, $body, $fonts, $globalCss) and — if you want third-party packages to keep injecting into your custom version — keep the two @stack directives.
Builder mount: the single HTML route
You do not need to build a host page anymore — the plugin ships exactly one Blade view (tagixo::builder.embed) reachable at:
GET /tagixo/builder/embed?type={type}&id={id}&back={url}
| Query | Required | Notes |
|---|---|---|
type |
yes | Registry key (pages, mails, pdfs, or your own custom key) |
id |
yes | Primary key of the record to edit |
back |
no | Absolute or relative URL injected as data-back-url; omit to hide the back button |
The route is dispatched by BuilderEmbedController (invokable). It looks up your BuilderTypeContract handler from the registry, calls its editorPayload($record) to fetch the URLs the Vue app needs (dataUrl / saveUrl / configUrl / context / layoutVariant), and renders the mount Blade. The Blade carries the right data-* attributes; the bundled JS wires tagixo:save → /tagixo/manage/{type}/{id}/save, tagixo:save-global-variables, tagixo:structure-changed → stylesheet refresh.
Your admin links the user at this URL. That's the whole integration on the editor side. From the perspective of your app, the flow is:
- User clicks "Edit" in your admin list.
- Browser navigates to
/tagixo/builder/embed?type=pages&id=42&back=/my-admin/pages. - The Vue SPA mounts, fetches data, lets the user edit, and posts saves back to the manage endpoint.
- The user clicks the back button (or any link of yours) and returns to your admin.
You do not embed iframes, you do not bundle the Vue assets yourself, you do not write a host page. The plugin owns the editor surface end-to-end; you own the admin chrome around it.
Editing strategy
Your admin only needs to:
- show a list of records (call your model directly, or
GET /tagixo/manage/{type}if you prefer JSON) - create / publish / delete via your own routes (or the plugin's REST endpoints)
- link / redirect to
/tagixo/builder/embed?type=…&id=…&back=…when the user wants to edit
The full sandbox example at ~/Development/visual-builder-test/tagixo-standalone-sandbox/ shows this pattern for every entity tagixo ships (Pages, Mails, PDFs, Menus, Layouts, Media) with six small Blade pages + six controllers, ~150 lines each. None of them mount the Vue builder themselves — they all link to the embed route.
If you need a non-standard host (admin panel inside an iframe, embedded in an external admin app, etc.), publish the Blade with php artisan vendor:publish --tag=tagixo-views and serve your own variant. The data-* attributes the JS expects are the contract; everything else is yours.
Seeding starter content
For first-run UX, create a seeder that generates a homepage with a full component tree.
Benefits:
- You guarantee a complete demo page after install.
- QA can verify rendering quickly.
- New customers can copy-and-adapt a production-like structure.
Operational recommendations
- Keep raw JSON as the source of truth.
- Treat rendered HTML/CSS as cacheable artifacts.
- Invalidate/rebuild artifacts on each content save.
- Version content if non-technical users can edit it.
- Keep authorization outside Tagixo internals.
- Add automated validation before persisting editor payloads.
Minimum standalone checklist
- content model exists
- JSON is cast correctly
- save path regenerates render artifacts
- publish path is separate from draft editing if required
- public page delivery does not depend on editor-only routes
- one seeded page renders end-to-end