Core Integration

Data Models and Global Variables

Expose reusable tokens and model metadata cleanly so editors can build without hardcoded values.

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:clear if 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