Template & data contract v1.2.0
The complete integration surface between this CMS and a site backend. Rendered from the same constants the compiler and gates run on, so this page cannot drift from reality. Decisions behind it: ADR-0005…0008 in docs/adr/.
1 · Slots — logic-free by design
Delivered templates contain no conditionals, loops, or expressions. A filler is mechanical string substitution — nothing more.
Inline product field: {{p:<SKU>:<field>}}
Inline store field: {{s:<CODE>:<field>}}
Inline context field: {{c:<bindingId>:<field>}} (the entity the host route serves)
Repeated block: {{#list:<bindingId>}} …inner uses {{f:<field>}}… {{/list}}
({{f:…}} resolves by the binding's entity)Product fields
title Product title, HTML-escaped
price Formatted price, e.g. "AED 145"
category Catalog category slug
stock "In stock" | "Low stock - N left" | "Out of stock"
image Product image URL (relative catalog path)
Store fields since v1.1.0
name Store name, HTML-escaped
address Street address
city City
phone Phone number
hours Opening hours text
services Comma-separated services
maplink Google Maps URL for the location
2 · Binding types
fixed Explicit SKUs in fixed order (curated campaign features). Selection frozen at authoring; values live at serve time. Requires a fallback policy.
query Rules-based — category / maxPriceAed / sort / limit — resolved fresh on every request. Self-heals when products disappear.
hybrid Pinned SKUs first, a query fills the remainder to a target count (merchandising rails).
static No data — baked content the filler never touches (artwork, editorial copy, countdowns).
context Since v1.2.0 — the single entity the host ROUTE is serving (PDP templates). The filler resolves it from request context; inline access via {{c:<bindingId>:<field>}}. CMS previews use the authoring mock.
external Since v1.2.0 — a list whose membership the FILLER decides (host-driven grids). The template defines item markup; the optional hint states editorial intent. CMS previews use the authoring mocks.
Entities since v1.1.0
product Default. Pinned by skus; queries by category / maxPriceAed / sort / limit.
store Pinned by codes; queries by city / limit. No fallback policies — a missing store fails the integrity gate.
Unavailability policies (required on pinned product bindings)
hide Omit the item; the layout collapses gracefully.
show Render it with its out-of-stock state.
substitute Replace with the first in-stock result of the fallback query not already on the page.
3 · Manifest — the structured half of every page
Ships beside the template in every artifact. Declares every binding the page needs; the filler resolves exactly what is declared, nothing else.
{
"contract": "1.2.0",
"meta": { "title": "Rioja Tasting Weekend - MMI", "description": "Three landmark Riojas, 15-31 August." },
"bindings": [
{ "id": "featured", "type": "fixed",
"skus": ["MMI-RIO-001", "MMI-RIO-002", "MMI-RIO-003"],
"fallback": { "policy": "substitute", "query": { "category": "rioja", "maxPriceAed": 120 } } },
{ "id": "grid", "type": "query",
"query": { "category": "whisky", "sort": "price_desc", "limit": 8 } },
{ "id": "branch", "entity": "store", "type": "fixed", "codes": ["MMI-MARINA"] },
{ "id": "finder", "entity": "store", "type": "query", "query": { "city": "Dubai" } },
{ "id": "product", "type": "context" },
{ "id": "grid1", "type": "external", "hint": "primary category grid, host-curated" }
]
}
// Note: in the AUTHORING manifest, context/external bindings also carry a
// "mock" field (sample data for CMS preview). It is stripped from delivered
// artifacts — what you receive is exactly the structure above.4 · Canonical entity shapes
What the adapter returns and what fill fields derive from (schema.org-aligned naming; see ADR-0007/0018).
product: { "sku": "MMI-RIO-001", "title": "Marques de Riscal Reserva 2019",
"category": "rioja", "price_aed": 145, "stock": 62 }
store: { "code": "MMI-MARINA", "name": "MMI Dubai Marina",
"address": "Marina Walk, Dubai Marina", "city": "Dubai",
"lat": 25.0772, "lng": 55.1369, "phone": "+971 4 368 2596",
"hours": "Sat-Thu 10:00-22:00; Fri 14:00-22:00",
"services": "Click and collect, Chilled room, Tastings" }5 · Publish gates
Every artifact passes all of these before it can be staged or published. Failures return to the authoring chat.
contract Contract & manifest — Manifest parses, matches the pinned contract version, and every {{#list:ID}} block has a declared binding.
tokens Design tokens only — No hex colors or literal font-family in page code — all color/type through var(--...) tokens, so design-doc changes restyle every page.
a11y Accessibility — Alt text on every image; exactly one h1.
perf Performance budget — Template under 60 KB and at most 12 images.
seo SEO metadata — manifest.meta.title present and description of 20+ characters.
integrity Bindings resolve — Every pinned/query binding resolves against the live catalog: pinned SKUs/codes exist, queries return at least one item. context/external bindings are host-resolved and outside this guarantee (ADR-0019).
js Script & asset policy — Self-contained vanilla JS only — no external scripts, eval, inline handlers, or cross-origin fetch; images only from the app CDN or catalog paths.
6 · Delivery & the filler's duties
Artifacts are immutable and content-hashed. On publish we send a webhook; the site backend pulls /api/delivery/<site>, caches the artifact, and activates it atomically. Rollback on either side is re-pointing at a previous hash.
At serve time the filler must, and may only: resolve each declared binding from the live catalog (for context: the entity the route is serving; for external: the filler's own selection per the hint), substitute slot values (HTML-escaped), apply unavailability policies on pinned items, inject tokensCss, and honor the publish/expiry window in meta (timezone Asia/Dubai).
Conformance: golden pages + fixture data run in both sides' CI (ADR-0014). Green kit = preview fidelity guaranteed.