Reference

CRM Sync — UI Component & ID Registry

Canonical naming + delivery model for the storefront UI system (nav, footer, cart, login, search) across design-sync.myshopify.com → crm-sync.dev. One addressable crm- namespace so every surface, collection, and bundle lines up.

Status: working reference. Endpoints marked ✅ are live; ⏳ are planned.


1. Delivery model — three app bundles, loaded from the CF Worker

CRM, PIM, and Design each ship as a separate component bundle served from the Cloudflare Worker's /embed/* endpoints. Updating a bundle = a worker deploy; the storefront never re-pastes code. (Layer 4 of the Higher-Order Stack.)

BundleLoadsEndpoint (worker)Placement
Stack (Layer 1)GA4 (Consent-Mode-v2, id from /stack/config) + UIkit/GSAP/petite-vue per-need/embed/stack-loader.js ✅helmet (<head>)
CRM<crm-brand-studio>, <crm-brand>, <crm-sync> (event bus)/embed/crm-elements.js ✅defer
PIM<pim-*> + crmPim.resolve() GID⇄Webflow⇄Xano/embed/pim-elements.js ✅defer
Designbrand theme / tokens (Layer 2 look)/brand/<slug>/theme.css ✅helmet
Docs modalwindow.crmDocsModal (docs utility)Pages docs-nav.js (data-modal-only)defer

Bundles are served from the worker, so an update ships with wrangler deploy — the storefront never re-pastes code. The Stack loader is window.crmStack (data-stack="uikit", data-worker, data-shop; GA4 pulled from /stack/config per-shop, MP secret server-side). The Brand Designer loads the SAME loader with data-stack="uikit,petite-vue,gsap".

Placement rule: the Nav loads in the helmet (head — no FOUC, paints first). The Footer loads deferred (defer) at end of body and carries the Shopify / CRM Web Components for Login + Cart.


2. ID registry (the crm- namespace)

Header nav (shopify-uikit-nav.liquid)

ElementidclassNotes
Nav wrapper / mount—.crm-navpetite-vue mount root
Search#crm-nav-search.crm-nav-searchUIkit search icon → routes.search_url
Cart#crm-nav-cart.crm-nav-cartUIkit cart icon + [data-cart-count] badge
Login#crm-nav-login.crm-nav-login→ routes.account_login_url
CTA—.crm-nav-ctaprimary button
Offcanvas (mobile)#crm-nav-offcanvas—UIkit offcanvas
Mobile search / cart / login#crm-nav-search-m · #crm-nav-cart-m · #crm-nav-login-minside offcanvas
ElementidNotes
Footer wrapper#crm-footerrender root
Login (web component)#crm-nav-loginshared login id — <crm-login> / Shopify Web Component
Cart (web component)#crm-nav-cartshared cart id — <crm-cart>
Footer nav mount#crm-footer-navfrom GET /nav?menu=footer

Cart and Login share one id each across nav + footer so a single cart/login component instance binds regardless of which surface triggered it.

Collections (Webflow → sync → worker)

CollectionList element idItem / link classMenu key
Nav Menu (header)#crm-nav-collection.crm-nav-item / .crm-nav-linkmain
Footer Menu#crm-footer-collection.crm-footer-item / .crm-footer-linkfooter
Tags → Category#crm-category-collection.crm-category-item(category tables)

Collection field slugs (what the sync reads): title, url, active, locale (+ optional order, group).


3. Data endpoints (worker)

EndpointMethodAuthPurpose
/stack/config?shop=GETpublicGA4 measurement ID (public subset only) ✅
/nav?shop=&menu=&locale=GETpublicnamed menu (main/footer), localized ✅
/nav?shop=&menu=&locale=POSTadmin/tenantwrite a menu (Webflow sync / config app) ✅
/flow/campaignPOSTadmin/tenantShopify Flow action → GA4 Smart Bidding (consent-gated, revenue-weighted) ✅
/categories?shop=&locale=&kind=GET/POSTpublic / adminTags as Category Collection ✅ (kind filter; mirrors /nav)

menu resolution: nav_menus[menu].i18n[locale] → [lang] → .items. Each locale is a separately-editable instance (English + globalized variants).


4. Tags = Category Collection Tables

Tags are modeled as a Category Collection — the same collection→sync→worker pattern as nav, backed by the category tables (Xano channels(201) / channel_membership(202) / category_pivot(203)). A Webflow "Category" collection (#crm-category-collection) syncs to /categories, and category-driven UI (filters, tag chips, audience membership) reads from it — one authoring surface, localized, projected to every surface.


5. Where each piece goes (paste map)

PieceTheme locationLoading
Stack loader + UIkit CSS + nav <style><head> (helmet)blocking CSS, async JS
Nav markuptop of body / sectionserver-rendered Liquid
Footer markup + CRM/PIM elementsbefore </body>defer
Brand theme.css (Design)<head>blocking

Long-term: the worker's Theme App Extension app embed injects the helmet + deferred bundles, so there's no manual theme editing.


6. Triggers & Actions → SSR Functions

Four axes are modeled as Triggers whose Actions execute in server-side functions (worker functions / Shopify Functions) — never in theme JS. They ride the consent-aware event bus (Tier A), are fail-closed, and are audit-logged.

AxisTrigger (fires on…)Action (server-side)SSR function / seam
Brandbrand/theme selected or publishedre-theme — emit theme.css / tokens for the surface/brand/<slug>/theme.css · Design bundle
QA / PRODpromote between realms (Stage → Prod → Deploy-Live)gate + swap env-scoped config/creds; fail-closed if connections/approvals incomplete/brand/<slug>/promote · env-label config
Personarole / entitlement change (Designer, QA, Release Eng)cap check at the data plane; hide-by-cap in UI, enforce in workerentitlements(190) · hasCap() / userIsDesignerForBrand()
Eventconsent change · cart · AP2 mandate · A2A delegationrun the handler (project to GA4/ESB, gate the cart, settle)consent-gated dataLayer bus → worker/Shopify Functions

The rule: a Trigger is a condition on the bus; its Action is a deterministic server-side function (input-bounded, side-effect-scoped), re-checked per loop. Presentation (nav/footer/components) only reflects the result — it never decides. This is why the same trigger holds whether a human or an agent fired it.

Backs onto: release personas / separation-of-duties, the brand env realms (Stage-Agency / Prod-Agency / Deploy-Live), and entitlement_changes(193) / AP2 agentic_checkout on the event bus.


7. Identity spine — every Shopify GID ⇄ same-name Webflow item

Invariant: every Shopify GID (gid://shopify/Product/…, Collection, Customer, Order, …) has a same-name item in Webflow, joined to a Xano row as the durable source of truth. One entity, three representations, one natural key.

   SHOPIFY gid://…  ⇄  XANO row (SoR, natural key + gid)  ⇄  WEBFLOW item (same name/slug)
                         ▲            sockets (CF Worker)            ▲
                         └──────── AI / CF / Xano orchestration ─────┘

Rules (from the ORM discipline in PROCESS-MANAGEMENT-DATA-LAYER.md):

This makes every entity agent-addressable and consent-qualified end to end: an AI agent resolves a Shopify GID → Xano row → Webflow item (and back) through one worker socket, under caps + consent, per loop.


8. Semantic wrapper — machine legibility (AEO + a11y)

The UIkit / semantic wrapper is what makes every #crm- id machine-legible. Because uk-* is BEM-namespaced (conflict-free by construction) it supplies the semantic skeleton that utility CSS can't; on top of it we attach a role + attribute pattern keyed to the id so screen readers and answer engines parse the same structure.

Pattern per addressable element:

LayerCarriesExample (#crm-nav-cart)
idthe addressid="crm-nav-cart"
ARIA role / labela11y semantics (WCAG)aria-label="Cart"
data-crm-rolemachine role for AEO / agentsdata-crm-role="cart"
data-crm-regionlandmark on the wrapper<nav … data-crm-region="primary-nav">
UIkit BEM classcomponent structure.uk-navbar-item

Applied on the nav today: data-crm-region="primary-nav" on the <nav> landmark; data-crm-role="search|cart|login" on the controls (+ native role/aria-label). Same pattern extends to footer, category chips, and every <crm-*> component — one wrapper, three readers (browser, screen reader, answer engine / agent).

Scores against the a11y (WCAG) + machine-index (AEO) harness — the wrapper is how we keep both high without hand-tuning each surface.