---
title: "CRM Sync — UI Component & ID Registry"
description: "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…"
canonical: https://persephonepunch.github.io/crm-sync-setup/ui-component-registry.html
category: "Specs"
date: 2026-07-07
source: https://github.com/persephonepunch/crm-sync-setup/blob/master/UI-COMPONENT-REGISTRY.md
licence: CC-BY-4.0
tags:
  - theme
  - webflow
  - shopify
  - architecture
---
# 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.)

| Bundle | Loads | Endpoint (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` |
| **Design** | brand theme / tokens (Layer 2 look) | `/brand/<slug>/theme.css` ✅ | helmet |
| **Docs modal** | `window.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`)
| Element | id | class | Notes |
|---|---|---|---|
| Nav wrapper / mount | — | `.crm-nav` | petite-vue mount root |
| Search | `#crm-nav-search` | `.crm-nav-search` | UIkit `search` icon → `routes.search_url` |
| Cart | `#crm-nav-cart` | `.crm-nav-cart` | UIkit `cart` icon + `[data-cart-count]` badge |
| Login | `#crm-nav-login` | `.crm-nav-login` | → `routes.account_login_url` |
| CTA | — | `.crm-nav-cta` | primary button |
| Offcanvas (mobile) | `#crm-nav-offcanvas` | — | UIkit offcanvas |
| Mobile search / cart / login | `#crm-nav-search-m` · `#crm-nav-cart-m` · `#crm-nav-login-m` | | inside offcanvas |

### Footer (`shopify-uikit-footer.liquid` ⏳)
| Element | id | Notes |
|---|---|---|
| Footer wrapper | `#crm-footer` | render root |
| Login (web component) | `#crm-nav-login` | shared login id — `<crm-login>` / Shopify Web Component |
| Cart (web component) | `#crm-nav-cart` | shared cart id — `<crm-cart>` |
| Footer nav mount | `#crm-footer-nav` | from `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)
| Collection | List element id | Item / link class | Menu key |
|---|---|---|---|
| Nav Menu (header) | `#crm-nav-collection` | `.crm-nav-item` / `.crm-nav-link` | `main` |
| Footer Menu | `#crm-footer-collection` | `.crm-footer-item` / `.crm-footer-link` | `footer` |
| 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)

| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
| `/stack/config?shop=` | GET | public | GA4 measurement ID (public subset only) ✅ |
| `/nav?shop=&menu=&locale=` | GET | public | named menu (`main`/`footer`), localized ✅ |
| `/nav?shop=&menu=&locale=` | POST | admin/tenant | write a menu (Webflow sync / config app) ✅ |
| `/flow/campaign` | POST | admin/tenant | Shopify Flow action → GA4 Smart Bidding (consent-gated, revenue-weighted) ✅ |
| `/categories?shop=&locale=&kind=` | GET/POST | public / admin | Tags 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)

| Piece | Theme location | Loading |
|---|---|---|
| Stack loader + UIkit CSS + nav `<style>` | `<head>` (helmet) | blocking CSS, `async` JS |
| Nav markup | top of body / section | server-rendered Liquid |
| Footer markup + CRM/PIM elements | before `</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.

| Axis | Trigger (fires on…) | Action (server-side) | SSR function / seam |
|---|---|---|---|
| **Brand** | brand/theme selected or published | re-theme — emit `theme.css` / tokens for the surface | `/brand/<slug>/theme.css` · Design bundle |
| **QA / PROD** | promote between realms (Stage → Prod → Deploy-Live) | gate + swap env-scoped config/creds; fail-closed if connections/approvals incomplete | `/brand/<slug>/promote` · env-label config |
| **Persona** | role / entitlement change (Designer, QA, Release Eng) | cap check at the **data plane**; hide-by-cap in UI, enforce in worker | entitlements(190) · `hasCap()` / `userIsDesignerForBrand()` |
| **Event** | consent change · cart · AP2 mandate · A2A delegation | run 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`):
- **Stable natural key** (SKU/handle/name), not a platform id, is the join — the
  item survives being re-created in any one platform. The GID is carried as an
  attribute, not the key.
- **Xano is the source of truth**; Shopify and Webflow are projections. Data flows
  **out of** Xano; no channel is the only place a fact exists.
- **One writer per field** (price → Xano, published-state → Webflow, fulfillment →
  Shopify); versioned upserts, idempotent on `(natural_key, source)`.
- **Sockets = CF Worker** owns the mapping + idempotency + conflict resolution;
  **AI** orchestrates match/normalize/enrich; **Xano** persists. Same seam the nav
  / footer / category collections ride — the collection items are just GID-keyed
  rows projected to each surface.

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:

| Layer | Carries | Example (`#crm-nav-cart`) |
|---|---|---|
| **id** | the address | `id="crm-nav-cart"` |
| **ARIA role / label** | a11y semantics (WCAG) | `aria-label="Cart"` |
| **`data-crm-role`** | machine role for AEO / agents | `data-crm-role="cart"` |
| **`data-crm-region`** | landmark on the wrapper | `<nav … data-crm-region="primary-nav">` |
| **UIkit BEM class** | component 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.
