Database and Models
Tagixo ships a complete set of Eloquent models backed by tables prefixed with tgx_. Every model lives under Ccast\Tagixo\Models\ and is registered through the package's service provider — you do not need to publish or reference them by path. The package also ships all migrations; running php artisan migrate after installing the package is all that is required.
You are not forced to use these models. Standalone integrations can store builder JSON in their own tables and ignore the package models entirely. They exist as a convenient default for projects that don't want to design their own schema.
Namespace: all models are in
Ccast\Tagixo\Models\. Facades and helpers resolve them through the container — you will rarely need to import them directly in application code, but do so freely when querying from controllers, jobs, or artisan commands.
Migrations
The package bundles all migrations internally. By default they load automatically, controlled by tagixo.auto_load_migrations in config/tagixo.php. If you want a physical copy in your repo — to customise column types, add indexes, or track schema changes in git — publish them first:
php artisan vendor:publish --tag=tagixo-migrations
After publishing, disable auto-loading so the package does not try to run the same migrations twice:
// config/tagixo.php
'auto_load_migrations' => false,
Run migrations the normal way:
php artisan migrate
# or inside Sail
./vendor/bin/sail artisan migrate
The package includes create migrations and incremental alter migrations. If you publish only the creates and modify them, future package updates may ship additional alter migrations that reference columns you have renamed — keep that in mind before editing published migrations.
When upgrading the package, re-run vendor:publish --tag=tagixo-migrations --force to bring in any new migration files. Schema changes are additive whenever possible; soft-deleted rows are preserved across version bumps.
Table inventory
| Table | Model | Notes |
|---|---|---|
tgx_pages |
Page |
Primary content store |
tgx_layouts |
Layout |
Header / body / footer wrappers |
tgx_menus |
Menu |
Navigation menus |
tgx_menu_items |
MenuItem |
Nestable links within a menu |
tgx_forms |
FormSchema |
Form-builder definitions |
tgx_sliders |
Slider |
Carousel / slideshow entities |
tgx_global_variables |
GlobalVariable |
Design tokens (colors, fonts, numbers) |
tgx_custom_fonts |
CustomFont |
Uploaded web font variants |
tgx_media |
Media |
Uploaded files and crop variants |
tgx_mail_templates |
MailTemplate |
Builder-authored email templates |
tgx_pdf_templates |
PdfTemplate |
Builder-authored PDF templates |
tgx_builder_templates |
BuilderTemplate |
Full-page reusable designs |
tgx_builder_library_items |
BuilderLibraryItem |
Section / row / column presets |
tgx_global_blocks |
GlobalBlock |
Shared builder subtrees (reference model) |
tgx_site_scripts |
SiteScript |
Per-location raw script injection |
tgx_site_settings |
SiteSettings |
Key-value site configuration |
tgx_popups |
Popup |
Popup builder entities |
PageStatus enum
The PageStatus backed string enum is shared by Page, MailTemplate, and PdfTemplate:
use Ccast\Tagixo\Enums\PageStatus;
// Cases
PageStatus::Draft // 'draft'
PageStatus::Published // 'published'
PageStatus::Scheduled // 'scheduled'
PageStatus::Archived // 'archived'
| Case | label() |
color() |
|---|---|---|
Draft |
Draft | gray |
Published |
Published | success |
Scheduled |
Scheduled | warning |
Archived |
Archived | danger |
color() returns a Primix / Filament-compatible badge color string. Use both helpers in custom admin columns or display logic anywhere in your application.
Content models
Content models are the primary storage layer for builder-authored content.
Page — tgx_pages
The central model. Every URL the builder manages maps to a Page record.
Columns
| Column | Type | Notes |
|---|---|---|
title |
string |
Human-readable title shown in admin |
slug |
string UNIQUE |
URL segment; unique across the table |
excerpt |
text |
Optional short description |
content |
json |
Full builder payload ({components, body}) |
rendered_html |
longText |
Cached render; null until first render |
css |
longText |
Cached CSS; null until first render |
meta_title |
string |
SEO title override |
meta_description |
text |
SEO description |
og_image |
string |
Open Graph image path |
canonical_url |
string |
Canonical URL override |
robots |
enum |
index,follow / noindex,follow / index,nofollow / noindex,nofollow |
status |
PageStatus |
Cast to PageStatus enum |
published_at |
datetime |
Publish-from window; null = immediate |
published_until |
datetime |
Publish-until window; null = no expiry |
parent_id |
FK→tgx_pages |
Self-referential hierarchy; nullOnDelete |
order |
int |
Sibling sort order |
metadata |
json |
Arbitrary extensible bag |
template_type |
string |
static / archive / single / specific |
model_class |
string |
FQCN of bound Eloquent model for dynamic pages |
model_id |
bigint |
Specific record id (for specific template_type) |
model_url_key |
string |
URL parameter attribute (default id; set to slug etc.) |
content defaults to {"components":[],"body":[]} — the canonical empty builder payload. Records can be created without passing content explicitly.
Casts
'content' => 'array',
'metadata' => 'array',
'status' => PageStatus::class,
'published_at' => 'datetime',
'published_until' => 'datetime',
Relationships
$page->parent(); // BelongsTo(Page) — immediate parent
$page->children(); // HasMany(Page) — children ordered by 'order'
Scopes
Page::published() // status=published AND inside the publish window
Page::root() // whereNull('parent_id')
Page::userManaged() // whereNull('source') — excludes plugin-seeded pages
The published() scope checks all three conditions atomically:
WHERE status = 'published'
AND (published_at IS NULL OR published_at <= NOW())
AND (published_until IS NULL OR published_until >= NOW())
Accessors
$page->is_published // bool — status=Published AND inside the window
$page->url // string — hierarchical URL built by walking parent chain
Methods
// Resolve the effective layout via LayoutResolver service
$page->getEffectiveLayout(): ?Layout
// Toggle publish state
$page->publish();
$page->unpublish();
// Dynamic page — check and resolve bound model
$page->isDynamic(): bool // template_type in [single, specific]
$page->resolveRecord(string|int|null $key): mixed // looks up model_class by key or model_id
// Render to a full standalone HTML string
$page->renderFull(?string $layoutView = null): string
// Wrap renderFull() in an HTTP response
$page->toResponse(int $status = 200, array $headers = []): Response
// Bypass the published() gate for signed builder preview URLs
Page::isAuthorizedPreviewRequest(Request $request): bool
Cache invalidation: when the builder saves a page, rendered_html and css are cleared automatically. The next call to renderFull() or PageRenderer::renderWithLayout() rebuilds them. If you update content programmatically, clear the cache yourself:
$page->update(['rendered_html' => null, 'css' => null]);
Dynamic pages (template_type other than static): these are template pages that bind to an Eloquent model. The archive type renders a listing context; single resolves a record from the URL path (/{slug}/{key}); specific is always bound to the single record at model_id. The model is not resolved automatically — call resolveRecord() in your controller:
// PublicPageController
$page = Page::published()->where('slug', $slug)->firstOrFail();
$record = $page->isDynamic()
? $page->resolveRecord(request()->route('key'))
: null;
if ($page->isDynamic() && $record === null) {
abort(404);
}
return $page->toResponse();
Soft deletes:
PageusesSoftDeletes. Trashed pages are excluded from all default queries. Thepublished()scope does not addwhereNotNull('deleted_at')explicitly because the global soft-delete scope already handles it.
MailTemplate — tgx_mail_templates
Builder-authored transactional email templates. Uses the same PageStatus enum as Page.
Columns
| Column | Type | Notes |
|---|---|---|
name |
string |
Admin label |
slug |
string UNIQUE |
Machine identifier |
subject |
string |
Default email subject |
preheader |
string |
Preview text shown in client list view |
content |
json |
Builder payload (defaults to empty payload) |
rendered_html |
longText |
Cached render |
css |
longText |
Cached CSS |
status |
PageStatus |
Cast to enum |
published_at |
datetime |
Publish-from date |
metadata |
json |
Extensible bag |
Key method
// Returns email-client-safe HTML with inlined CSS
$template->renderHtml(): string
// Typical usage from a Mailable:
Mail::html(
MailTemplate::where('slug', 'welcome')->firstOrFail()->renderHtml(),
fn ($m) => $m->to($user)->subject('Welcome aboard')
);
The MailRenderer is optimised for the mail context — it excludes layout wrappers and outputs a 600 px-constrained table structure rather than the full page layout. See mail-and-pdf-templates for the full rendering pipeline.
Soft deletes:
MailTemplateusesSoftDeletes.
PdfTemplate — tgx_pdf_templates
Same shape as MailTemplate with three additional columns that drive the @page CSS rule:
| Column | Type | Default |
|---|---|---|
paper_size |
string |
A4 |
orientation |
string |
portrait |
margin |
string |
2cm |
// Returns print-ready HTML; feed to any PDF engine
$html = PdfTemplate::where('slug', 'invoice')->firstOrFail()->renderHtml();
// Browsershot (Puppeteer wrapper):
Browsershot::html($html)->save(storage_path('app/invoice.pdf'));
// DomPDF:
return Pdf::loadHTML($html)->download('invoice.pdf');
Soft deletes:
PdfTemplateusesSoftDeletes.
Layout and design models
Layout models control the structural wrappers applied to pages and the reusable design library available in the builder sidebar.
Layout — tgx_layouts
Header / body / footer wrappers applied to pages. The layout wraps the page content slot — the page's own modules render between the layout's header and footer.
Columns
| Column | Type | Notes |
|---|---|---|
name |
string |
Admin label |
is_global |
boolean |
Default false; only one global layout allowed at a time |
conditions |
json |
Condition rules for automatic layout assignment |
header_content |
json |
Builder payload for the header zone |
body_content |
json |
Builder payload for the body wrapper zone |
footer_content |
json |
Builder payload for the footer zone |
header_css |
text |
Cached CSS for header |
body_css |
text |
Cached CSS for body wrapper |
footer_css |
text |
Cached CSS for footer |
header_rendered_html |
longText |
Cached header HTML |
body_rendered_html |
longText |
Cached body wrapper HTML |
footer_rendered_html |
longText |
Cached footer HTML |
Static helper
Layout::global(): ?Layout // returns the single global layout, or null
Boot behaviour
- Saving a layout with
is_global = trueautomatically unsets the flag on all other layouts — only ONE global layout exists at any time. - Attempting to delete the global layout throws
\RuntimeException. Reassign the global flag to another layout before deleting.
Layout resolution: pages no longer carry a layout_id foreign key (removed in a later alter migration). The LayoutResolver service evaluates each layout's conditions and returns the first matching one, falling back to Layout::global(). Call $page->getEffectiveLayout() to retrieve the resolved layout from any context.
BuilderTemplate — tgx_builder_templates
Full reusable page designs used by the "Save as template" / "Load template" affordances in the builder.
Columns: name, description, content (json), context (page|form|mail|pdf), thumbnail.
Scopes: forContext(string), pages(), forms(), mails(), pdfs(), ordered().
BuilderLibraryItem — tgx_builder_library_items
Section / row / column level presets. Intended for reusing layout pieces, not whole pages.
Columns: name, description, type (section|row|column), category, content (json), context, is_global (boolean).
When is_global = true the item is treated as a shared reference — editing it in one place propagates to all pages that embed it.
GlobalBlock — tgx_global_blocks
Builder subtrees (sections, rows, columns, or individual modules) stored once and referenced by id. Pages hold a placeholder node carrying global_block_id rather than a copy of the content — editing the block updates every page that references it.
Columns: title, slug, type, category, content (json), rendered_html, css, status.
Scopes: published(), draft().
Method: isEmpty(): bool — returns true when content.components is empty.
Soft deletes:
GlobalBlockusesSoftDeletes. Trashing a global block leaves orphan placeholder nodes on pages — the renderer falls back gracefully to rendering nothing for missing global blocks, but clean up references before trashing.
Navigation models
Menu — tgx_menus
Named navigation menus. A menu is a container; the actual links live in MenuItem records.
Columns: name, slug, description, css_class, metadata (json).
Relationships
$menu->items() // HasMany(MenuItem) — root items only, ordered by 'order'
$menu->allItems() // HasMany(MenuItem) — all items regardless of nesting level
$menu->visibleItems() // HasMany(MenuItem) — root visible items only, ordered
Static helper
Menu::findBySlug('main-navigation'): ?Menu
Soft deletes:
MenuusesSoftDeletes. Deleting a menu cascades to all itsMenuItemrecords via the database FK constraint (cascadeOnDelete).
MenuItem — tgx_menu_items
Individual links within a menu, nestable to arbitrary depth via parent_id.
Columns
| Column | Type | Notes |
|---|---|---|
menu_id |
FK→tgx_menus |
Parent menu; cascadeOnDelete |
parent_id |
FK→tgx_menu_items |
Nullable self-ref for nesting; cascadeOnDelete |
label |
string |
Display text |
target_type |
MenuItemTargetType |
url / page / route / anchor |
target_value |
string |
The link value (URL string, page slug/id, route name, or anchor id) |
target_meta |
json |
Extra data; route params when target_type=route |
new_tab |
boolean |
Open in new tab (target="_blank") |
icon |
string |
Icon identifier (Blade Icons pack) |
css_class |
string |
Extra CSS classes on the <a> element |
dropdown_type |
string |
null = standard dropdown; custom type identifiers supported |
order |
int |
Sibling sort order |
visible |
boolean |
Whether the item is rendered in the public menu |
MenuItemTargetType enum
| Case | Value | Resolved as |
|---|---|---|
Url |
url |
Raw string, may be external |
Page |
page |
Page slug or id → $page->url |
Route |
route |
Laravel named route + target_meta params |
Anchor |
anchor |
# prepended to target_value |
Relationships
$item->menu() // BelongsTo(Menu)
$item->parent() // BelongsTo(MenuItem)
$item->children() // HasMany(MenuItem) ordered by 'order'
$item->visibleChildren() // HasMany(MenuItem) visible=true, ordered
Methods
// Resolve the final URL (handles all target_type cases; returns null on failure)
$item->resolveUrl(): ?string
// True when the resolved URL points to a different host than the current request
$item->isExternal(): bool
// True when this item has nested children
$item->hasChildren(): bool
// Active-link detection
// "/" matches only the home root; other paths match prefix
// (e.g. "/blog" stays active on "/blog/post-1")
// External links, pure anchors, and URL-less items always return false
$item->isActiveForPath(string $currentPath): bool
// Typical rendering loop:
$menu = Menu::findBySlug('primary');
$currentPath = request()->getPathInfo();
foreach ($menu->visibleItems as $item) {
$url = $item->resolveUrl();
$active = $item->isActiveForPath($currentPath);
$target = $item->new_tab ? '_blank' : '_self';
// render $url, $active, $target ...
}
Form model
FormSchema — tgx_forms
Form definitions authored in the form builder. Submissions are not persisted to the database by default — on submit, the configured form action runs (send email, webhook, etc.).
Columns
| Column | Type | Notes |
|---|---|---|
title |
string |
Admin label |
slug |
string UNIQUE |
Used as form_key in public routes |
description |
text |
Optional description |
content |
json |
Form-level body props (settings, styles) |
fields |
json |
Ordered array of field component definitions |
metadata |
json |
Extensible bag |
status |
string |
draft / published / archived |
form_target |
string |
Denormalised from content.body.form_target.value; default universal |
The form_target column is denormalised: the model's saving boot hook extracts it from the content payload automatically. Do not set it manually — it will be overwritten on the next save.
Scopes
FormSchema::published() // status = 'published'
FormSchema::draft() // status = 'draft'
Method
$form->isEmpty(): bool // true when fields array is empty
Global and media models
These models back the design system and media library used across all builder contexts.
GlobalVariable — tgx_global_variables
User-defined design tokens emitted as CSS custom properties on :root. The renderer outputs --tgx-{key}: {value} for every variable, making them available to any module's CSS without further configuration.
Columns
| Column | Type | Notes |
|---|---|---|
key |
string UNIQUE |
CSS variable name fragment — becomes --tgx-{key} |
name |
string |
Human-readable label |
type |
string |
color / font / number / link / image |
group |
string |
Admin grouping for the variable picker |
value |
json |
Type-specific value bag |
order |
int |
Display order within the group |
is_protected |
boolean |
Protected variables cannot be deleted from the admin |
Scopes: colors(), fonts(), numbers(), links(), images(), ordered().
The package ships a GlobalVariablesSeeder that creates default tokens from the package config. Run it once on a fresh install:
php artisan db:seed --class="Ccast\\Tagixo\\Database\\Seeders\\GlobalVariablesSeeder"
Or call it from your own DatabaseSeeder:
$this->call(\Ccast\Tagixo\Database\Seeders\GlobalVariablesSeeder::class);
CustomFont — tgx_custom_fonts
Uploaded web font variants available in the builder's font picker.
Columns: family, weight (100–900), style (normal|italic), format (woff2|woff|ttf|otf), file_path, original_name, file_size.
Accessor: $font->url — resolves the public Storage URL for the font file.
Static helper: CustomFont::getFamilies() returns variants grouped by family name. Use it to build @font-face CSS blocks in your layout:
foreach (CustomFont::getFamilies() as $family => $variants) {
foreach ($variants as $variant) {
// emit @font-face { font-family: ...; src: url(...); }
}
}
Media — tgx_media
Uploaded files and their crop / size variants. Crop variants are stored as child records with parent_id pointing to the original.
Columns
| Column | Type | Notes |
|---|---|---|
uuid |
uuid UNIQUE |
Stable external reference; use this in serialised data |
filename |
string |
Stored filename on disk |
original_filename |
string |
Original upload filename |
path |
string |
Storage path |
disk |
string |
Laravel filesystem disk (default public) |
mime_type |
string |
Full MIME type |
extension |
string(10) |
File extension |
size |
bigint |
Bytes |
width |
uint |
Pixel width (images only) |
height |
uint |
Pixel height (images only) |
alt_text |
string |
Accessibility alt text |
title |
string |
Optional display title |
description |
text |
Optional description |
folder |
string |
Virtual folder path |
metadata |
json |
Extensible bag (EXIF, focal point, etc.) |
parent_id |
FK→tgx_media |
Parent record for crop / size variants; cascadeOnDelete |
uploaded_by |
FK→users |
Uploader user id; nullOnDelete |
The media gallery, picker fields, and MediaPropType all reference media by uuid. See media-gallery for the full upload and crop lifecycle.
Miscellaneous models
Slider — tgx_sliders
Carousel / slideshow entities. Each slider is a self-contained builder context with its own slides as components within the content payload.
Columns: title, slug, content (json), rendered_html, css, status.
Scopes: published(), draft(). Method: isEmpty(): bool.
Soft deletes:
SliderusesSoftDeletes.
Popup — tgx_popups
Standalone popup definitions authored in their own builder context. Popups are referenced by id from the Section or page-wrapper Advanced settings tab. The Popup model follows the same shape as Slider.
SiteScript — tgx_site_scripts
Raw script / markup blobs injected verbatim into every public page at a specific DOM location. Useful for analytics tags, Google Tag Manager, verification meta, or chat widgets.
Locations: head, body_open, body_close.
// In your Blade layout — echoes raw HTML:
{!! \Ccast\Tagixo\Models\SiteScript::html('head') !!}
{!! \Ccast\Tagixo\Models\SiteScript::html('body_open') !!}
{!! \Ccast\Tagixo\Models\SiteScript::html('body_close') !!}
html() returns an HtmlString safe for {!! !!}. It returns an empty string when the location is unknown, the script is disabled, or the table does not exist yet (mid-migration boot).
SiteSettings — tgx_site_settings
Key-value store for global site configuration (site name, logo, favicon, contact email, etc.). Access is via static helpers:
SiteSettings::get('site_name', 'My Site');
SiteSettings::set('site_name', 'Tagixo Website');
App-level models that reference Tagixo tables
Consumer applications often need to reference builder content from their own Eloquent models — for example, a Product that links to its detail Page, or a BlogPost backed by a Page for rendering.
The idiomatic approach is to add a nullable foreign key and a BelongsTo relation:
// Migration in your app
Schema::table('products', function (Blueprint $table) {
$table->foreignId('page_id')
->nullable()
->constrained('tgx_pages')
->nullOnDelete();
});
// Product model
use Ccast\Tagixo\Models\Page;
class Product extends Model
{
public function page(): BelongsTo
{
return $this->belongsTo(Page::class, 'page_id');
}
}
For dynamic pages (using template_type = 'single'), the relationship is inverted — the Page record holds model_class and model_url_key, and the router resolves your model from the URL key at request time. Your PublicPageController calls $page->resolveRecord($keyFromUrl) and makes the result available to the view or renderer data context.
Do not add domain models for content that belongs in the builder. Form submissions, page content, and layout data all live in Tagixo's own tables. Your app models should reference builder records, not duplicate their columns.
When writing custom queries against Tagixo tables from your controllers or jobs, always import the model class explicitly — do not construct raw DB::table('tgx_*') queries unless you have a specific performance reason, as raw queries bypass casts, scopes, and soft-delete behaviour:
use Ccast\Tagixo\Models\Page;
use Ccast\Tagixo\Enums\PageStatus;
// Correct — respects all scopes and casts
$pages = Page::published()->root()->orderBy('order')->get();
// Avoid — bypasses SoftDeletes, casts, and scopes
$pages = DB::table('tgx_pages')->where('status', 'published')->get();
See also
- Storage model and data shape — builder JSON payload structure
- Data models and global variables — runtime variable resolution
- Rendering and preview workflow —
PageRendererand cache lifecycle - Media gallery — upload, crop, and
tgx_medialifecycle - Mail and PDF templates —
MailRendererand PDF engine integration - Contexts, layout variants and builders — form, slider, popup builder contexts
- Laravel bootstrap and configuration —
config/tagixo.phpreference