Data Models and Global Variables
Two subsystems make builder integrations maintainable at scale: global variables and registered data models. Global variables are site-wide design tokens — colors, fonts, brand values — that editors reference from any style picker and that the renderer emits as CSS custom properties on every page render. Registered data models tell the builder which data sources editors can bind list-entry modules to, and how to fetch live records at render time.
Global Variables
Global variables are stored in the tgx_global_variables table as key/value pairs with a declared type. At render time, PageRenderer reads every row and injects them as CSS custom properties into the page's <head>. In the builder, color pickers and font pickers are populated from the matching-type rows, so editors pick by name rather than by raw hex value.
Seeding Global Variables
The package ships Ccast\Tagixo\Database\Seeders\GlobalVariablesSeeder, which inserts a baseline set of color and typography tokens. Run it once after migration:
php artisan db:seed --class="Ccast\Tagixo\Database\Seeders\GlobalVariablesSeeder"
To extend or override with your own brand values, create a seeder that upserts into tgx_global_variables:
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class BrandVariablesSeeder extends Seeder
{
public function run(): void
{
$variables = [
['key' => 'color_primary', 'value' => '#1d4ed8', 'type' => 'color', 'label' => 'Primary'],
['key' => 'color_secondary', 'value' => '#6d28d9', 'type' => 'color', 'label' => 'Secondary'],
['key' => 'color_surface', 'value' => '#ffffff', 'type' => 'color', 'label' => 'Surface'],
['key' => 'font_heading', 'value' => 'Inter', 'type' => 'font', 'label' => 'Heading Font'],
['key' => 'font_body', 'value' => 'Inter', 'type' => 'font', 'label' => 'Body Font'],
['key' => 'brand_site_name', 'value' => 'Acme', 'type' => 'text', 'label' => 'Site Name'],
];
foreach ($variables as $variable) {
DB::table('tgx_global_variables')->updateOrInsert(
['key' => $variable['key']],
$variable + ['created_at' => now(), 'updated_at' => now()]
);
}
}
}
| Column | Type | Description |
|---|---|---|
key |
string |
Unique identifier. Used as the CSS custom property name (prefixed with --tgx-). |
value |
string |
The raw value: hex for colors, family name for fonts, plain string for text/numbers. |
type |
string |
color, font, text, or number. Controls which builder picker surfaces this row. |
label |
string |
Human-readable name shown in the builder picker. |
CSS Output
PageRenderer emits all global variables as CSS custom properties inside a <style> block in <head>, scoped to :root:
<style>
:root {
--tgx-color_primary: #1d4ed8;
--tgx-color_secondary: #6d28d9;
--tgx-font_heading: Inter;
}
</style>
Modules that reference a global variable store the key (e.g. color_primary) in their payload. The renderer resolves it to var(--tgx-color_primary) in the emitted CSS, so changing a variable's value in the database is reflected across every page on next render with no page re-save required.
Managing Variables at Runtime
If you build an admin UI for variables outside of the builder, update tgx_global_variables directly via Eloquent:
use Ccast\Tagixo\Models\GlobalVariable;
GlobalVariable::updateOrCreate(
['key' => 'color_primary'],
['value' => '#2563eb', 'type' => 'color', 'label' => 'Primary']
);
After bulk-updating variables, call
php artisan optimize:clearif you cache config or views. The renderer reads variables from the database on every request, so no additional cache-busting is needed for the CSS output itself.
Registering Data Models
The List Entry module renders repeating content — article cards, product grids, team members — by fetching records at render time through a callback you register. The registry lives in PHP memory; there is no database table for it.
Registering a Model Source
Call Tagixo::registerModel() in a service provider. The first argument is the registry key (used in builder payloads), the second is a callable that returns a metadata array describing the source and its fields:
use Ccast\Tagixo\Facades\Tagixo;
// app/Providers/AppServiceProvider.php
public function boot(): void
{
Tagixo::registerModel('articles', function () {
return [
'label' => 'Articles',
'fields' => [
['key' => 'title', 'label' => 'Title', 'type' => 'text'],
['key' => 'excerpt', 'label' => 'Excerpt', 'type' => 'text'],
['key' => 'slug', 'label' => 'Slug', 'type' => 'text'],
['key' => 'published_at', 'label' => 'Published At', 'type' => 'text'],
['key' => 'cover_image', 'label' => 'Cover Image', 'type' => 'image'],
['key' => 'url', 'label' => 'URL', 'type' => 'link'],
],
];
});
}
The fields array describes the attributes the builder exposes in its field-binding picker. The type value controls which prop types the builder allows binding to:
type |
Binds to |
|---|---|
text |
Text, heading, and label props |
image |
Image src props |
link |
Link href props |
color |
Color props |
Runtime Record Fetching
The registry callback is also responsible for returning the actual records when the List Entry module renders. Register a second callable for record resolution via Tagixo::registerModelResolver():
Tagixo::registerModelResolver('articles', function (array $options): array {
$query = \App\Models\Article::query()
->where('published_at', '<=', now())
->orderByDesc('published_at');
if (!empty($options['limit'])) {
$query->limit((int) $options['limit']);
}
return $query->get()->map(fn($article) => [
'title' => $article->title,
'excerpt' => $article->excerpt,
'slug' => $article->slug,
'published_at' => $article->published_at?->toDateString(),
'cover_image' => $article->cover_image_url,
'url' => route('articles.show', $article->slug),
])->all();
});
The $options array is passed from the List Entry module's builder configuration. It may contain:
| Key | Type | Description |
|---|---|---|
limit |
int |
Maximum number of records to return. |
order_by |
string |
Field key to sort by. |
order_dir |
string |
asc or desc. |
filters |
array |
Arbitrary key/value pairs set by the editor in the builder. |
Your resolver is called on every page render that contains a bound List Entry module. Keep queries efficient — eager-load relationships, avoid N+1 patterns, and consider caching for high-traffic pages.
A Complete Registration Example
Tagixo::registerModel('products', function () {
return [
'label' => 'Products',
'fields' => [
['key' => 'name', 'label' => 'Name', 'type' => 'text'],
['key' => 'price', 'label' => 'Price', 'type' => 'text'],
['key' => 'description', 'label' => 'Description', 'type' => 'text'],
['key' => 'image', 'label' => 'Image', 'type' => 'image'],
['key' => 'url', 'label' => 'URL', 'type' => 'link'],
],
];
});
Tagixo::registerModelResolver('products', function (array $options): array {
return \App\Models\Product::query()
->where('active', true)
->orderBy('sort_order')
->limit($options['limit'] ?? 12)
->get()
->map(fn($p) => [
'name' => $p->name,
'price' => '$' . number_format($p->price / 100, 2),
'description' => $p->short_description,
'image' => $p->primary_image_url,
'url' => route('products.show', $p->slug),
])->all();
});
Content Tokens
Content tokens are {{...}} placeholders resolved server-side by PageRenderer before the page HTML is returned. They work in any text or heading content authored in the builder.
Built-in Tokens
| Token | Resolves to |
|---|---|
{{year}} |
Current four-digit year (e.g. 2025) |
{{date:Y-m-d}} |
Current date formatted with PHP date() format string |
{{date:d F Y}} |
e.g. 15 July 2025 |
{{site.name}} |
Value of the brand_site_name global variable, or config('app.name') as fallback |
{{site.url}} |
config('app.url') |
Custom Tokens
Register custom tokens in a service provider using Tagixo::registerToken(). The second argument is a callable that returns the replacement string:
use Ccast\Tagixo\Facades\Tagixo;
public function boot(): void
{
Tagixo::registerToken('support.email', fn() => config('mail.from.address'));
Tagixo::registerToken('stats.articles', fn() => (string) \App\Models\Article::published()->count());
Tagixo::registerToken('user.name', function () {
return auth()->check() ? auth()->user()->name : 'Guest';
});
}
Editors then type {{support.email}}, {{stats.articles}}, or {{user.name}} directly into any text field in the builder. At render time, PageRenderer performs a single pass over all page content and replaces every registered token.
Custom token callables are executed on every page render that contains the token. Keep them fast: cache expensive lookups with
Cache::remember()rather than hitting the database on every request.
Token Resolution Order
PageRenderer resolves tokens in this order: built-in date/year tokens first, then site.* tokens, then custom registered tokens. If two tokens share the same key (e.g. you register a custom year token), the built-in wins. Prefix your custom tokens with a namespace to avoid collisions — brand.year, app.version, etc.
See also
- Storage Model and Data Shape — page/layout/menu table schemas and how builder payloads are stored
- Module System and Catalog — how the List Entry module reads registered model resolvers
- Standalone Page Builder Integration — bootstrap/save/preview endpoints and the builder Blade layout
- Production Checklist and Troubleshooting — caching, performance, and render diagnostics