YYayaw

CMS Data Models

Typed, localized, publishable data models and inline variables for CMS content.

Overview

Yayaw separates CMS content data from design tokens.

  • /dashboard/content/design-tokens remains the design-token editor.
  • /dashboard/content/data-models manages scoped CMS data model structure.
  • /dashboard/content/data fills and publishes entries for those models.

CMS data models can be scoped:

  • global: shared site-level data controlled by global-data:manage.
  • organization: active-organization data controlled by page:manage or sections:manage on that organization.

Media-like values should be stored as URLs or strings. The organization media library remains scoped separately. Use Media Library for organization-owned binary assets and store only the resulting URLs in global or organization data entries.

CMS Role

CMS data is the structured source layer for content that needs a stable contract: site variables, menus, product facts, organization facts, reusable collections, and fields that pages or sections bind by reference. Data entries should hold values, not presentation. Sections and pages decide how those values render.

Use data models when authors need typed fields, localization, validation, and a published lifecycle. Use media assets for files, sections for reusable visual composition, and design tokens for visual theme values.

Document Contracts

Global data uses two versioned JSON documents:

  • GlobalDataModelDocumentV1
  • GlobalDataEntryDocumentV1

Model documents define:

  • cardinality: singleton or collection
  • fields: typed fields inspired by Builder custom fields
  • localization: default locale and supported locales
  • presentation: headless, builtin_renderer, or component_recipe

Cardinality controls how many entries a model can own:

  • singleton: one global entry, for example header-menu, footer-menu, or default SEO settings.
  • collection: many entries, for example products, authors, redirects, or reusable announcements.

Presentation controls how data is consumed:

  • headless: structured data only, consumed by pages, bindings, helpers, or custom code.
  • builtin_renderer: data rendered by Yayaw runtime components, currently used by first-party models such as header and footer.
  • component_recipe: data intended to be paired with a reusable UI recipe in a later workflow.

Each field supports the Builder-style settings editors expect:

  • type
  • localization
  • default value
  • helper text
  • required/optional
  • enum values for select fields
  • hidden fields that stay out of entry editing while preserving values/defaults

Hidden required fields must define a default value so generated entry editors cannot create impossible drafts.

Supported V1 field types are:

  • text, long_text, url, file
  • number, boolean, select, color
  • rich_text, html, date, timestamp
  • list, reference, map, javascript, code, tags, json

file stores a URL/string in V1 because the media library is organization-scoped while global data is site-wide.

Entry documents store values as either:

  • shared: one value for every locale
  • localized: values per locale with fallback from requested locale to default locale, then first available locale

Storage

CMS data uses immutable revisions and one publication row per registry item.

Tables:

  • global_data_model_registry_items
  • global_data_model_revisions
  • global_data_model_publications
  • global_data_entry_registry_items
  • global_data_entry_revisions
  • global_data_entry_publications

Revisions are hash-deduped and validated before publish. Publications can be draft, published, or archived.

global_data_model_registry_items stores scope and organization_id. Existing rows are migrated to scope = "global". Slugs are unique per global scope or per organization scope.

Dashboard Editing

The dashboard provides:

  • model list using Yayaw Table
  • full-width model field editor
  • dedicated entry list on /dashboard/content/data
  • generated inspector-style entry editor based on the selected model fields
  • save, publish, and archive actions
  • read-only rendering for users without manage permission

Editing requires:

  • global-data:manage for global models.
  • page:manage or sections:manage on the active organization for organization models.

Read-only access is available through:

  • global-data:read or global-data:list for global models.
  • page:read, page:list, sections:read, or sections:list for organization models.

Runtime APIs

Server helpers live in:

  • src/lib/server/services/global-data

Key helpers:

  • getPublishedGlobalDataEntry
  • getPublishedGlobalDataValue
  • listPublishedGlobalDataEntries
  • listPublishedGlobalDataFieldReferences

Most helpers accept scope and organizationId when organization-scoped data is needed. Existing page-builder global_data_field and global_data_query bindings continue to resolve global data.

The shared runtime slot component lives in:

  • src/blocks/shared/global-data-slot.tsx

It resolves a published singleton entry and falls back to the existing static UI when no published data is available.

Header and footer are no longer seeded section catalog entries or editable page props on every page.

The seed script creates two singleton built-in renderer models:

  • header-menu
  • footer-menu

Both receive a default main entry.

Generated page headers and footers bind to these singleton entries with global_data_field props. Pages therefore share one localized source of truth for brand labels, locale-specific brand URLs, menu items, footer tagline, and copyright text.

The items_json field is edited with a visual menu editor in the global data entry form on /dashboard/content/data. It supports top-level links, groups with one nested link level, theme toggles, language toggles, account actions, reorder controls, and a raw JSON recovery panel. Header items can target the left, center, or right zones; the default entry uses the left brand link, center navigation, and right-side theme/language/account actions. The default header-menu/main and footer-menu/main entries organize public navigation into two first-level groups:

  • Yayaw: /[locale]/codebase, /[locale]/pricing, and /[locale]/docs
  • Yayaw Table: /[locale]/table, /[locale]/table/example, /[locale]/docs/table, and /[locale]/docs/table/installation

Keep Yayaw Table developer documentation links under the Fumadocs subproject at /[locale]/docs/table/*. CMS-authored /[locale]/table/* pages remain the public product and demo surfaces. Old site-header, site-footer, builtin-header, and builtin-footer seed rows are deleted by migration. Legacy page documents that still reference the old built-in sections are normalized into generated header/footer nodes.

Page Builder Bindings

Page prop bindings support global data in addition to literals and media assets.

Binding kinds:

  • global_data_field: binds one field from one published entry
  • global_data_query: resolves a published collection and optionally extracts one field

The Puck inspector exposes a "Bind data" control when published global data references are available. V1 binds complete fields.

Inline CMS variables can also be inserted into text values from the dashboard header catalog on /dashboard/content/* routes. Tokens are namespaced to avoid collisions:

  • {site.name}
  • {organization.logo}
  • {data.global.header-menu.main.brand_label}
  • {data.organization.<modelSlug>.<entrySlug>.<fieldKey>}
  • {billing.product.pro_monthly.name}
  • {billing.price.pro_monthly.id}

Unknown tokens remain unchanged at runtime. Non-namespaced tokens such as {name} are ignored in V1.

Site variables are edited through the CMS Data entry for the built-in site-variables/main singleton model. Its default fields are locked so the editing surface can change values without deleting the canonical site.* contract. Each field still keeps its own localization setting: technical values such as base_url, domain, and logo URLs stay shared, while editorial values such as description and title can be localized and resolve according to the requested locale. Existing system_settings values under cms.site-variables.v1 remain as migration and fallback data.

Billing product and price variables are read from the runtime billing product catalog. Product metadata comes from billing_products when configured, while Stripe price IDs are stored internally after the admin or MCP catalog sync. Organization variables are read from the Better Auth organization row; editing continues to live in organization settings flows.

The built-in billing-product-content global collection complements the billing catalog with one CMS entry per product key. Its locked catalog_product_key field links the CMS entry to Products & Services; editorial fields such as badge, summary, feature bullets, CTA label, and highlight state can be localized. Do not store product names, prices, intervals, Stripe IDs, or checkout availability in this model. Resolve those facts from the billing.product.* and billing.price.* variable namespaces.

The seed also carries the Supabase-exported global data snapshot for production site variables, header and footer menus, and the sales-offer/main singleton. These entries are published on every seed run so fresh production or preview databases resolve the same CMS variables that were authored in Supabase. The billing-product-content entries are still created only when missing so later editorial product copy is not overwritten by an empty baseline seed.

Cache Invalidation

Published global data is cached with tags:

  • global-data
  • global-data:{modelSlug}

Publish and archive actions revalidate the global data tags, dashboard data routes, the public root path, and the published page runtime cache.

Validation

Useful checks after CMS data model changes:

bun test src/lib/shared/global-data/global-data.test.ts
bun run check
bunx tsc --noEmit