Skip to content

Data shape

The fundamental design principle of tt-blocks: the editor → D1 → metafield → __TTB_BLOCKS__ → storefront rendering five layers all share the same config object shape, guarded by 22 audit scripts.

createDefault(type, locale) ← initial component defaults
├─ preset:* → engine + @widget/presets (wizard preset path)
└─ single → COMPONENT_DEFS[type].createDefault
↓ both paths land on the same canonical defaults
editor reducer state ← app/lib/widget-state.jsx
↓ merchant edits fields in the editor
D1.widgets.config_json + metafield ← saveWidget
↓ Shopify pushes metafield to storefront theme
window.__TTB_BLOCKS__[id] ← injected by widget.liquid
↓ widget.js reads config and renders
WidgetRenderer (Preact tree) ← packages/widget-ui

Every layer reads the same shape — what the merchant sees in the admin editor preview is what renders on the storefront.

Stored in D1 widgets.config_json and mirrored to Shopify app.metafields.widget[<key>]:

{
"key": "ttb-xxxxxxxx",
"content": [ /* top-level component tree */ ],
"container": { /* wrapper styling */ },
"enabled": true,
"userCss": "",
"version": "1",
"breakpoint": "768px",
"hideElements": [],
"customAnchorSelectors": {
"product": [], "productList": [], "productMedia": [], "exclude": []
},
"locales": {},
"placement": { /* see reference/placement-modes */ },
"visibility": { /* see editor/display 9 dimensions */ }
}
FieldPurpose
keyWidget identifier, matches D1 widget_key column
contentTop-level component array — see Widget structure
containerWidget wrapper background / border / radius / padding / margin / displayAnimation
enabledWidget master switch
userCssMerchant-supplied CSS (Pro only)
versionSchema version, currently "1"
breakpointMobile breakpoint, default "768px"
hideElementsCSS selectors to hide theme elements (Pro only)
customAnchorSelectorsMerchant-supplied theme DOM selectors (escape hatch, max 24)
localesi18n translation map: { locale: { "componentId.path": "translated text" } }
placementMount location — see Placement modes
visibilityDisplay rules — see Display tab

GSC-compatible fields vs project extensions

Section titled “GSC-compatible fields vs project extensions”

The tt-blocks data shape largely aligns with GSC Blinks (the open-source reference implementation), enabling fixture interop and validation.

FieldSourceNotes
key / content / container / enabled / version / breakpoint / hideElements / localesGSC-compatibleSame shape as GSC fixtures
userCssGSC-compatibleSame field name as GSC
placementProject extensionNo GSC equivalent — describes where widget mounts
visibilityProject extensionNo GSC equivalent — describes who sees the widget
customAnchorSelectorsProject extensionMerchant override for the default 65-theme selector matrix

All persistence layers (D1 / metafield / __TTB_BLOCKS__) are strictly add-only:

  • ✅ Add optional fields (with default fallbacks)
  • ❌ Remove existing fields
  • ❌ Change field types
  • ❌ Promote optional → required

Reason: existing merchant widgets must not break when the schema evolves; the renderer must read all historical versions.

Enforced by widget-schema-evolution-audit — any non-additive PR fails CI. See the 7 forward-compat contracts in Multi-axis Token Bundle.