@bacano/sdk 1.0.1 → 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +439 -18
- package/STOREFRONT_CLARITY.md +140 -0
- package/STOREFRONT_CONSTRUCTION.md +105 -0
- package/STOREFRONT_LEADS.md +130 -0
- package/STOREFRONT_LEGAL.md +66 -0
- package/STOREFRONT_MARKETING.md +291 -0
- package/STOREFRONT_PERFORMANCE.md +304 -0
- package/STOREFRONT_PREVIEW.md +499 -0
- package/STOREFRONT_PRODUCT_SPECIFICATIONS.md +127 -0
- package/STOREFRONT_VOLUME_PRICING.md +131 -0
- package/client.d.ts +8 -1
- package/errors.d.ts +9 -1
- package/graphql.cjs +266 -3
- package/graphql.d.ts +2 -2
- package/graphql.js +266 -3
- package/index.cjs +2035 -255
- package/index.d.ts +8 -6
- package/index.js +2035 -255
- package/marketing/attribution.d.ts +53 -0
- package/marketing/clarity.d.ts +3 -0
- package/marketing/domain.d.ts +1 -0
- package/marketing/ga4.d.ts +2 -0
- package/marketing/index.d.ts +5 -0
- package/marketing/runtime.d.ts +2 -0
- package/marketing/sanitize.d.ts +2 -0
- package/marketing/types.d.ts +111 -0
- package/media.d.ts +45 -2
- package/modules/auth.d.ts +3 -2
- package/modules/availability.d.ts +2 -2
- package/modules/cart-errors.d.ts +2 -0
- package/modules/cart-session.d.ts +2 -2
- package/modules/cart.d.ts +4 -3
- package/modules/catalog.d.ts +15 -3
- package/modules/content.d.ts +2 -2
- package/modules/marketing.d.ts +5 -0
- package/modules/orders.d.ts +2 -2
- package/modules/profile.d.ts +1 -1
- package/modules/static-catalog.d.ts +13 -0
- package/modules/website.d.ts +7 -1
- package/package.json +14 -3
- package/react/context-registry.d.ts +9 -0
- package/react/context.d.ts +1 -1
- package/react/hooks.d.ts +24 -2
- package/react/marketing-context.d.ts +3 -0
- package/react/marketing-provider.d.ts +10 -0
- package/react/preview/cache.d.ts +23 -0
- package/react/preview/context.d.ts +5 -0
- package/react/preview/navigation.d.ts +9 -0
- package/react/preview/provider.d.ts +3 -0
- package/react/preview/shell.d.ts +3 -0
- package/react/preview/toolbar.d.ts +3 -0
- package/react/preview/types.d.ts +60 -0
- package/react/preview.cjs +742 -0
- package/react/preview.d.ts +14 -0
- package/react/preview.js +730 -0
- package/react/website-gate.d.ts +22 -0
- package/react.cjs +3893 -1795
- package/react.d.ts +8 -4
- package/react.js +3891 -1789
- package/skills/bacano-web/SKILL.md +116 -0
- package/skills/bacano-web/agents/openai.yaml +4 -0
- package/skills/bacano-web/references/architecture-runtime.md +109 -0
- package/skills/bacano-web/references/common.md +92 -0
- package/skills/bacano-web/references/informational.md +39 -0
- package/skills/bacano-web/references/report.md +70 -0
- package/skills/bacano-web/references/storefront.md +116 -0
- package/skills/bacano-web/scripts/audit-http.mjs +284 -0
- package/skills/bacano-web/scripts/cli.mjs +222 -0
- package/skills/bacano-web/scripts/postinstall.mjs +8 -0
- package/transport/documents.d.ts +23 -18
- package/transport/graphql.d.ts +13 -1
- package/types.d.ts +338 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Marketing y medición en storefronts Bacano
|
|
2
|
+
|
|
3
|
+
Este documento es el contrato canónico para agentes y equipos que construyen un
|
|
4
|
+
storefront con `@bacano/sdk`. Una tienda no debe inventar nombres de eventos,
|
|
5
|
+
identificadores, adaptadores ni llamadas directas a proveedores.
|
|
6
|
+
|
|
7
|
+
## Regla principal
|
|
8
|
+
|
|
9
|
+
El storefront expresa hechos mediante `useMarketing().track()`. El SDK decide
|
|
10
|
+
si la medición está activa, sanea la URL y adapta el evento a GA4, Meta y
|
|
11
|
+
Microsoft Clarity. Nunca
|
|
12
|
+
se debe llamar directamente a `gtag`, `dataLayer`, `fbq`, Measurement Protocol
|
|
13
|
+
o Conversions API.
|
|
14
|
+
Clarity también se carga desde este runtime; no se instala su snippet de forma
|
|
15
|
+
paralela.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { useMarketing } from "@bacano/sdk/react";
|
|
19
|
+
|
|
20
|
+
const marketing = useMarketing();
|
|
21
|
+
|
|
22
|
+
marketing.track({
|
|
23
|
+
eventName: "contact",
|
|
24
|
+
contactMethod: "whatsapp",
|
|
25
|
+
contactPlacement: "product_detail",
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Un contacto general usa el mismo contrato sin inventar un producto:
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
marketing.track({
|
|
33
|
+
eventName: "contact",
|
|
34
|
+
contactMethod: "email",
|
|
35
|
+
contactPlacement: "contact_page",
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
La caída o ausencia de marketing nunca puede bloquear catálogo, carrito,
|
|
40
|
+
checkout, pago, autenticación o navegación. En localhost, preview y orígenes no
|
|
41
|
+
canónicos el runtime es un `no-op` seguro.
|
|
42
|
+
|
|
43
|
+
## Identidades estables
|
|
44
|
+
|
|
45
|
+
- `WebsiteMarketingItem.itemId` siempre es `product_variant_id` de Bacano.
|
|
46
|
+
- SKU es información complementaria; nunca reemplaza `itemId`.
|
|
47
|
+
- Un evento de producto debe usar la variante realmente visible o elegida.
|
|
48
|
+
- `order_id` es el `transaction_id` de conversiones autoritativas.
|
|
49
|
+
- Los nombres, precios, cantidades y atributos deben corresponder al mismo
|
|
50
|
+
instante y variante.
|
|
51
|
+
|
|
52
|
+
Estas reglas permiten que eventos, feeds de catálogo, públicos y conversiones
|
|
53
|
+
se relacionen sin depender del slug o de la ruta del storefront.
|
|
54
|
+
|
|
55
|
+
## Cuándo emitir cada evento
|
|
56
|
+
|
|
57
|
+
| Evento | Emitir después de | No emitir por |
|
|
58
|
+
| ------------------- | -------------------------------------- | ---------------------------------- |
|
|
59
|
+
| `page_view` | Navegación resuelta y renderizada | Cada render de React |
|
|
60
|
+
| `search` | Resultados válidos para una búsqueda | Cada tecla |
|
|
61
|
+
| `view_item_list` | Lista visible con items reales | Skeleton o respuesta fallida |
|
|
62
|
+
| `select_item` | Selección que inicia navegación válida | Hover |
|
|
63
|
+
| `view_item` | Producto y variante cargados | Cambio irrelevante de estado |
|
|
64
|
+
| `add_to_cart` | Carrito actualizado | Clic antes de conocer el resultado |
|
|
65
|
+
| `remove_from_cart` | Carrito actualizado | Intento fallido |
|
|
66
|
+
| `view_cart` | Carrito abierto con su contenido | Montaje global de providers |
|
|
67
|
+
| `begin_checkout` | Carrito sincronizado con Bacano | Abrir un modal sin sincronizar |
|
|
68
|
+
| `add_shipping_info` | Entrega elegida o confirmada | Valores predeterminados invisibles |
|
|
69
|
+
| `add_payment_info` | Método elegido o confirmado | Método aún inválido |
|
|
70
|
+
| `contact` | Apertura intencional del canal | Render del botón |
|
|
71
|
+
|
|
72
|
+
`purchase`, `order_placed` y `refund` son eventos autoritativos del backend. El
|
|
73
|
+
storefront nunca los emite desde una página de éxito o respuesta de pago.
|
|
74
|
+
|
|
75
|
+
## Payload de productos
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
marketing.track({
|
|
79
|
+
eventName: "add_to_cart",
|
|
80
|
+
value: unitPrice * quantity,
|
|
81
|
+
items: [
|
|
82
|
+
{
|
|
83
|
+
itemId: selectedVariant.id,
|
|
84
|
+
itemName: product.name,
|
|
85
|
+
sku: selectedVariant.sku,
|
|
86
|
+
price: unitPrice,
|
|
87
|
+
quantity,
|
|
88
|
+
itemBrand: product.brand?.name,
|
|
89
|
+
itemCategory: product.category,
|
|
90
|
+
itemVariant: selectedVariant.attributes
|
|
91
|
+
.map((attribute) => attribute.value)
|
|
92
|
+
.join(" / "),
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- `value` representa el valor total de los items del evento.
|
|
99
|
+
- `price` es unitario y `quantity` es positiva.
|
|
100
|
+
- No se deben enviar productos ocultos, precios aproximados ni la primera
|
|
101
|
+
variante por conveniencia si el comprador eligió otra.
|
|
102
|
+
- El storefront no envía PII dentro de eventos de navegador.
|
|
103
|
+
|
|
104
|
+
## Contactos y WhatsApp
|
|
105
|
+
|
|
106
|
+
Todo enlace de WhatsApp, teléfono o correo debe emitir `contact`. Esto mide la
|
|
107
|
+
apertura del canal; no demuestra que el comprador haya enviado un mensaje o que
|
|
108
|
+
el comercio lo haya atendido.
|
|
109
|
+
|
|
110
|
+
Valores de `contactMethod`:
|
|
111
|
+
|
|
112
|
+
- `whatsapp`
|
|
113
|
+
- `phone`
|
|
114
|
+
- `email`
|
|
115
|
+
|
|
116
|
+
Valores de `contactPlacement`:
|
|
117
|
+
|
|
118
|
+
- `product_detail`
|
|
119
|
+
- `floating`
|
|
120
|
+
- `header`
|
|
121
|
+
- `footer`
|
|
122
|
+
- `contact_page`
|
|
123
|
+
- `checkout`
|
|
124
|
+
- `other` únicamente cuando ninguna ubicación canónica describe el contacto
|
|
125
|
+
|
|
126
|
+
El campo es opcional, no acepta textos visibles ni valores libres y el runtime
|
|
127
|
+
lo ignora cuando el evento no es `contact`. Una ubicación nueva debe añadirse
|
|
128
|
+
primero al contrato oficial del SDK.
|
|
129
|
+
|
|
130
|
+
Un contacto asociado a un producto debe incluir la variante, cantidad y valor:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
marketing.track({
|
|
134
|
+
eventName: "contact",
|
|
135
|
+
contactMethod: "whatsapp",
|
|
136
|
+
contactPlacement: "product_detail",
|
|
137
|
+
value: unitPrice * quantity,
|
|
138
|
+
items: [
|
|
139
|
+
{
|
|
140
|
+
itemId: selectedVariant.id,
|
|
141
|
+
itemName: product.name,
|
|
142
|
+
sku: selectedVariant.sku,
|
|
143
|
+
price: unitPrice,
|
|
144
|
+
quantity,
|
|
145
|
+
itemBrand: product.brand?.name,
|
|
146
|
+
itemCategory: product.category,
|
|
147
|
+
itemVariant: variantLabel,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Un botón flotante o enlace general no debe atribuirse artificialmente a un
|
|
154
|
+
producto. GA4 recibe `method`, `contact_placement`, `items` y `value`; Meta
|
|
155
|
+
recibe el evento estándar `Contact`, `contact_method`, `contact_placement`,
|
|
156
|
+
`content_ids` y `contents` cuando existen items.
|
|
157
|
+
|
|
158
|
+
Clarity registra automáticamente el comportamiento visual de la sesión y
|
|
159
|
+
recibe `contact` como evento personalizado para filtrar grabaciones. No recibe
|
|
160
|
+
PII, items ni etiquetas construidas por el storefront. La guía específica está
|
|
161
|
+
en [`STOREFRONT_CLARITY.md`](./STOREFRONT_CLARITY.md).
|
|
162
|
+
|
|
163
|
+
## Fuente única y deduplicación
|
|
164
|
+
|
|
165
|
+
- Cada interacción tiene un único propietario de instrumentación.
|
|
166
|
+
- No se combinan handlers locales con seguimiento automático duplicado.
|
|
167
|
+
- `page_view` tiene una sola fuente para navegación inicial y SPA.
|
|
168
|
+
- Un cambio de cantidad puede expresarse como diferencia real de carrito; no se
|
|
169
|
+
vuelve a emitir todo el carrito como nuevas altas.
|
|
170
|
+
- Los efectos de React deben depender de identidades primitivas estables o
|
|
171
|
+
contar con una guarda explícita para evitar duplicados.
|
|
172
|
+
|
|
173
|
+
## URLs, atribución y privacidad
|
|
174
|
+
|
|
175
|
+
- No enviar URLs construidas manualmente; el runtime usa y sanea la ubicación
|
|
176
|
+
actual cuando `pageLocation` se omite.
|
|
177
|
+
- Nunca incluir email, teléfono, dirección, documento, tokens de pedido,
|
|
178
|
+
autenticación, OAuth o pago en nombres, URLs o parámetros.
|
|
179
|
+
- La atribución se obtiene con `marketing.getAttribution()` y se entrega al
|
|
180
|
+
contrato oficial de carrito/checkout.
|
|
181
|
+
- `bacano_origin=<key-publica>` puede identificar un origen personalizado
|
|
182
|
+
configurado en Bacano. El SDK lo conserva durante la navegación y el backend
|
|
183
|
+
lo valida contra la empresa antes de asignarlo a la venta.
|
|
184
|
+
- Si no existe esa key, Bacano normaliza UTM, referidor y click IDs a los
|
|
185
|
+
orígenes base; una venta web sin señal reconocida conserva `Web` como origen.
|
|
186
|
+
- La autorización publicitaria del checkout es opcional, separada y nunca
|
|
187
|
+
bloquea la compra.
|
|
188
|
+
- Los datos de contacto solo pueden normalizarse y hashearse en el backend bajo
|
|
189
|
+
las reglas de autorización de Bacano.
|
|
190
|
+
|
|
191
|
+
## Componentes que todo storefront debe auditar
|
|
192
|
+
|
|
193
|
+
- Provider raíz y una única fuente de `page_view`.
|
|
194
|
+
- Buscador, resultados, categorías, colecciones y tarjetas de producto.
|
|
195
|
+
- Detalle, selección de variante y recomendaciones si están dentro del alcance.
|
|
196
|
+
- Carrito: alta, baja, cantidad y vista.
|
|
197
|
+
- Checkout: inicio, entrega, pago y consentimiento.
|
|
198
|
+
- Todos los enlaces de WhatsApp, teléfono y correo.
|
|
199
|
+
- Preview: marketing desactivado en todo su árbol.
|
|
200
|
+
|
|
201
|
+
## Content Security Policy
|
|
202
|
+
|
|
203
|
+
La CSP pertenece al storefront y se genera durante el build o en el hosting.
|
|
204
|
+
El runtime no puede ampliar una cabecera que el navegador ya recibió. Se deben
|
|
205
|
+
incluir únicamente los proveedores activos del website y conservar los demás
|
|
206
|
+
orígenes que realmente use la aplicación.
|
|
207
|
+
|
|
208
|
+
Para GA4 y destinos de Google Ads vinculados al mismo Google tag:
|
|
209
|
+
|
|
210
|
+
```text
|
|
211
|
+
script-src https://www.googletagmanager.com
|
|
212
|
+
connect-src https://www.googletagmanager.com
|
|
213
|
+
https://www.google-analytics.com
|
|
214
|
+
https://*.google-analytics.com
|
|
215
|
+
https://analytics.google.com
|
|
216
|
+
https://*.analytics.google.com
|
|
217
|
+
https://stats.g.doubleclick.net
|
|
218
|
+
https://googleads.g.doubleclick.net
|
|
219
|
+
https://www.googleadservices.com
|
|
220
|
+
img-src https://www.google-analytics.com
|
|
221
|
+
https://googleads.g.doubleclick.net
|
|
222
|
+
https://www.googleadservices.com
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`https://www.googletagmanager.com` debe estar tanto en `script-src` como en
|
|
226
|
+
`connect-src`: Google tag puede usar `/td` mediante `fetch`, aunque el script
|
|
227
|
+
principal ya haya cargado correctamente.
|
|
228
|
+
|
|
229
|
+
Para Meta Pixel:
|
|
230
|
+
|
|
231
|
+
```text
|
|
232
|
+
script-src https://connect.facebook.net
|
|
233
|
+
connect-src https://www.facebook.com https://graph.facebook.com
|
|
234
|
+
img-src https://www.facebook.com
|
|
235
|
+
frame-src https://www.facebook.com
|
|
236
|
+
form-action 'self' https://www.facebook.com
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Meta puede enviar `/tr/` mediante un formulario POST dirigido a un iframe
|
|
240
|
+
oculto. Permitir `connect-src` o `img-src` no sustituye `form-action` y
|
|
241
|
+
`frame-src`. Usar el origen exacto; no abrir `*.facebook.com` ni comodines de
|
|
242
|
+
infraestructura para silenciar errores.
|
|
243
|
+
|
|
244
|
+
Para Microsoft Clarity:
|
|
245
|
+
|
|
246
|
+
```text
|
|
247
|
+
script-src https://*.clarity.ms
|
|
248
|
+
connect-src https://*.clarity.ms https://c.bing.com
|
|
249
|
+
img-src https://*.clarity.ms https://c.bing.com
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Clerk, Wompi, videos y cualquier otro proveedor conservan su propia lista de
|
|
253
|
+
orígenes. La CSP final es la unión mínima de las capacidades efectivamente
|
|
254
|
+
montadas. Probarla en consola, Network y, cuando sea posible, primero mediante
|
|
255
|
+
`Content-Security-Policy-Report-Only`.
|
|
256
|
+
|
|
257
|
+
## Validación obligatoria
|
|
258
|
+
|
|
259
|
+
Antes de declarar un storefront listo:
|
|
260
|
+
|
|
261
|
+
1. Ejecutar typecheck, lint, unit tests y build.
|
|
262
|
+
2. Probar que acciones fallidas no emitan eventos exitosos.
|
|
263
|
+
3. Confirmar que una acción produzca un solo evento.
|
|
264
|
+
4. Verificar IDs, variantes, cantidades, valores y moneda.
|
|
265
|
+
5. Revisar URLs para confirmar que no contienen tokens ni PII.
|
|
266
|
+
6. Validar GA4 en DebugView o tiempo real.
|
|
267
|
+
7. Validar Meta en Test Events y revisar `content_ids`.
|
|
268
|
+
8. Confirmar que preview y localhost no envíen eventos reales.
|
|
269
|
+
9. Si existe catálogo publicitario, confirmar que cada `itemId` coincide con el
|
|
270
|
+
`id` de la misma variante en la fuente de Meta o Google.
|
|
271
|
+
10. Si Clarity está activo, validar tag, recolección, sesiones y eventos
|
|
272
|
+
personalizados en el dominio canónico.
|
|
273
|
+
11. Confirmar que la CSP no bloquea los transportes reales de GA4, Meta o
|
|
274
|
+
Clarity, incluidos Google tag `/td` y Meta `/tr/` por POST/iframe.
|
|
275
|
+
|
|
276
|
+
## Checklist corto para agentes
|
|
277
|
+
|
|
278
|
+
- [ ] Leí este contrato antes de instrumentar.
|
|
279
|
+
- [ ] Uso únicamente `useMarketing().track()`.
|
|
280
|
+
- [ ] Uso `product_variant_id` como `itemId`.
|
|
281
|
+
- [ ] Emito eventos después del éxito, no antes.
|
|
282
|
+
- [ ] Instrumenté todos los contactos con método y ubicación.
|
|
283
|
+
- [ ] Los contactos de producto incluyen item, cantidad y valor.
|
|
284
|
+
- [ ] No emito conversiones autoritativas desde el navegador.
|
|
285
|
+
- [ ] No envío PII, tokens o secretos.
|
|
286
|
+
- [ ] No duplico `page_view` ni eventos por renders.
|
|
287
|
+
- [ ] Validé GA4, Meta, Clarity, preview y degradación segura.
|
|
288
|
+
|
|
289
|
+
La guía de gobierno, persistencia y eventos autoritativos vive en
|
|
290
|
+
`docs/websites/MARKETING_MEASUREMENT_STOREFRONTS_CHECKLIST.md` dentro del
|
|
291
|
+
repositorio Bacano. Este archivo es el contrato distribuible para storefronts.
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# Storefront performance contract
|
|
2
|
+
|
|
3
|
+
This is the canonical performance contract for storefronts built with
|
|
4
|
+
`@bacano/sdk`. It applies to static, hybrid and server-rendered storefronts.
|
|
5
|
+
|
|
6
|
+
## Required architecture
|
|
7
|
+
|
|
8
|
+
1. Create one `BacanoClient` and mount one `BacanoProvider` at the application
|
|
9
|
+
root. Do not create independent clients per component or route.
|
|
10
|
+
2. Generate catalog routes and their page data from one build snapshot. Never
|
|
11
|
+
call `getProductBySlug()` once for every product during the same build.
|
|
12
|
+
3. Render useful catalog and product HTML during the static/server render.
|
|
13
|
+
Loading skeletons are fallbacks, not the initial production document.
|
|
14
|
+
4. Use the SDK for every Bacano request. Storefronts must not duplicate GraphQL
|
|
15
|
+
documents, gateway limits, retries or storage transform rules.
|
|
16
|
+
5. Keep build data separate from volatile state. Prices and availability may be
|
|
17
|
+
refreshed after hydration with the lightweight live-state contract.
|
|
18
|
+
6. A live refresh must preserve the static content when it fails. A public page
|
|
19
|
+
must not become empty because Bacano is temporarily unavailable.
|
|
20
|
+
|
|
21
|
+
## Static catalog bootstrap
|
|
22
|
+
|
|
23
|
+
Use one bootstrap per website build:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
const bootstrap = await client.catalog.getStaticCatalogBootstrap({
|
|
27
|
+
categoryListKey: "categorias-web",
|
|
28
|
+
attributeListKey: "atributos",
|
|
29
|
+
pageSize: 100,
|
|
30
|
+
initialPageSize: 24,
|
|
31
|
+
maxProducts: 10_000,
|
|
32
|
+
sortBy: "name_asc",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Render this in the initial catalog HTML.
|
|
36
|
+
const initialCatalogPage = bootstrap.initialPage;
|
|
37
|
+
|
|
38
|
+
// Reuse the same objects for routes, metadata, JSON-LD and sitemap.
|
|
39
|
+
const productByRoute = new Map(
|
|
40
|
+
bootstrap.products.map((product) => [product.slug || product.id, product]),
|
|
41
|
+
);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The SDK paginates internally, respects `Retry-After`, rejects inconsistent
|
|
45
|
+
snapshots and avoids repeating facets on every page. Builds must fail clearly
|
|
46
|
+
if this contract fails; they must not silently publish an empty catalog.
|
|
47
|
+
|
|
48
|
+
### Listing variants and detail variants
|
|
49
|
+
|
|
50
|
+
The static bootstrap deliberately exposes two projections on each product:
|
|
51
|
+
|
|
52
|
+
- `product.variants` contains only variants that were saleable when Bacano
|
|
53
|
+
generated the catalog page. Cards, price ranges, filters and listing
|
|
54
|
+
visibility must use this projection.
|
|
55
|
+
- `product.detailVariants` also contains enabled, published variants with a
|
|
56
|
+
public price that are currently sold out. Product detail selectors must use
|
|
57
|
+
this projection so a customer can see unavailable sizes instead of assuming
|
|
58
|
+
they do not exist.
|
|
59
|
+
|
|
60
|
+
Use the public helper so cached snapshots from older SDK versions remain safe:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { getProductDetailVariants } from "@bacano/sdk";
|
|
64
|
+
|
|
65
|
+
const detailVariants = getProductDetailVariants(product);
|
|
66
|
+
const liveState = await client.catalog.getProductLiveState(
|
|
67
|
+
detailVariants.map((variant) => variant.id),
|
|
68
|
+
);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`getProduct()` and `getProductBySlug()` expose the complete detail projection
|
|
72
|
+
through `variants` as well. A sold-out variant returns `inStock: false` and
|
|
73
|
+
`maxPurchasableQuantity: 0` from live state. Never use `detailVariants` to
|
|
74
|
+
calculate a card's minimum price or to make a completely sold-out product
|
|
75
|
+
appear in listings.
|
|
76
|
+
|
|
77
|
+
## Live price and availability
|
|
78
|
+
|
|
79
|
+
After hydration, refresh only the variants visible on screen:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const liveState = await client.catalog.getProductLiveState(visibleVariantIds);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
One call accepts at most 100 unique variant IDs. Debounce filter/pagination
|
|
86
|
+
changes and do not refresh the whole static catalog. The response only contains
|
|
87
|
+
variant ID, current pricing, availability and `maxPurchasableQuantity`.
|
|
88
|
+
|
|
89
|
+
For a local cart, batch all distinct variant IDs when the cart opens and use
|
|
90
|
+
`maxPurchasableQuantity` as the total cap per variant, including units already
|
|
91
|
+
in the cart. Disable quantity increases at the cap and explain that the maximum
|
|
92
|
+
available was reached. Do not poll after every click or persist the limit as
|
|
93
|
+
catalog data: another checkout may consume stock, and Bacano remains
|
|
94
|
+
authoritative when checkout starts and finishes.
|
|
95
|
+
|
|
96
|
+
For websites configured with multiple inventory sources, the cap is the
|
|
97
|
+
largest quantity fulfillable by a single source, not the sum of all warehouses.
|
|
98
|
+
Bacano assigns each complete order line to the first prioritized warehouse that
|
|
99
|
+
can fulfill it. Storefronts must never choose, expose or combine warehouses.
|
|
100
|
+
|
|
101
|
+
Post-sale inventory follows the same ownership rules. A returned item goes back
|
|
102
|
+
to the warehouse that fulfilled the original order line. An outgoing replacement
|
|
103
|
+
for a web exchange is assigned to the first current website inventory source
|
|
104
|
+
that can fulfill the complete replacement quantity. Pausing the website does not
|
|
105
|
+
block post-sale processing for orders placed while it was active.
|
|
106
|
+
|
|
107
|
+
## Responsive media
|
|
108
|
+
|
|
109
|
+
Every SDK product/content media object can expose `responsiveSources`. Use it
|
|
110
|
+
with a realistic `sizes` value so mobile devices do not download desktop media:
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import { formatMediaSrcSet } from "@bacano/sdk";
|
|
114
|
+
|
|
115
|
+
<img
|
|
116
|
+
src={media.cardUrl || media.url}
|
|
117
|
+
srcSet={formatMediaSrcSet(media.responsiveSources ?? []) || undefined}
|
|
118
|
+
sizes="(max-width: 640px) 50vw, (max-width: 1200px) 33vw, 320px"
|
|
119
|
+
width={320}
|
|
120
|
+
height={400}
|
|
121
|
+
loading="lazy"
|
|
122
|
+
decoding="async"
|
|
123
|
+
alt={media.altText || product.name}
|
|
124
|
+
/>;
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The first visible product image may use eager loading/high priority. Images
|
|
128
|
+
below the fold and recommendation blocks must remain lazy. Storefronts must not
|
|
129
|
+
append storage transformation query parameters manually.
|
|
130
|
+
|
|
131
|
+
### Banners and full-width heroes
|
|
132
|
+
|
|
133
|
+
Editorial `ContentMedia` images additionally expose:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
type ContentMedia = {
|
|
137
|
+
src?: string | null;
|
|
138
|
+
url?: string | null;
|
|
139
|
+
originalUrl?: string | null;
|
|
140
|
+
thumbnailUrl?: string | null;
|
|
141
|
+
cardUrl?: string | null;
|
|
142
|
+
detailUrl?: string | null;
|
|
143
|
+
heroUrl?: string | null;
|
|
144
|
+
responsiveSources?: Array<{ width: number; url: string }>;
|
|
145
|
+
originalWidth?: number | null;
|
|
146
|
+
originalHeight?: number | null;
|
|
147
|
+
};
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`src` and `url` remain available as compatibility aliases. New storefront code
|
|
151
|
+
must select the semantic URL that matches its rendering context.
|
|
152
|
+
|
|
153
|
+
`heroUrl` is an optimized WebP fallback for wide editorial media. Bacano uses
|
|
154
|
+
the candidates `640`, `960`, `1400`, `1920` and `2560`, but removes every size
|
|
155
|
+
larger than the original file and adds the exact intrinsic width as the final
|
|
156
|
+
candidate. A 1920 px source therefore ends at 1920 px; a 1180 px source ends at
|
|
157
|
+
1180 px. When Bacano cannot determine the dimensions, `heroUrl` safely falls
|
|
158
|
+
back to `originalUrl` and no speculative `srcset` is emitted.
|
|
159
|
+
|
|
160
|
+
For separate mobile and desktop artwork, keep the breakpoint in the storefront
|
|
161
|
+
and let the browser choose density within each source:
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
import { formatMediaSrcSet, type ContentMedia } from "@bacano/sdk";
|
|
165
|
+
|
|
166
|
+
export function EditorialHero({
|
|
167
|
+
desktop,
|
|
168
|
+
mobile,
|
|
169
|
+
}: {
|
|
170
|
+
desktop: ContentMedia;
|
|
171
|
+
mobile: ContentMedia;
|
|
172
|
+
}) {
|
|
173
|
+
return (
|
|
174
|
+
<picture>
|
|
175
|
+
<source
|
|
176
|
+
media="(max-width: 767px)"
|
|
177
|
+
srcSet={formatMediaSrcSet(mobile.responsiveSources ?? []) || undefined}
|
|
178
|
+
sizes="100vw"
|
|
179
|
+
/>
|
|
180
|
+
<img
|
|
181
|
+
src={desktop.heroUrl ?? desktop.originalUrl ?? ""}
|
|
182
|
+
srcSet={formatMediaSrcSet(desktop.responsiveSources ?? []) || undefined}
|
|
183
|
+
sizes="100vw"
|
|
184
|
+
width={desktop.originalWidth ?? undefined}
|
|
185
|
+
height={desktop.originalHeight ?? undefined}
|
|
186
|
+
alt={desktop.alt ?? ""}
|
|
187
|
+
decoding="async"
|
|
188
|
+
/>
|
|
189
|
+
</picture>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
This contract works in static exports: all URLs are deterministic, cacheable
|
|
195
|
+
gateway URLs and the browser selects the appropriate candidate at runtime for
|
|
196
|
+
device-pixel ratios 1 and 2. Do not use `detailUrl` as the fallback for a
|
|
197
|
+
full-width hero and do not append `w`, `q` or `f` parameters in a storefront.
|
|
198
|
+
|
|
199
|
+
## Request ownership
|
|
200
|
+
|
|
201
|
+
- Website initialization: obtain one public configuration and use it to seed the
|
|
202
|
+
SDK context. Do not resolve the same website again through GraphQL.
|
|
203
|
+
- Catalog build: one paginated bootstrap.
|
|
204
|
+
- Initial catalog navigation: zero mandatory catalog requests after hydration;
|
|
205
|
+
the static page is already usable.
|
|
206
|
+
- Live state: one batched request for visible variants.
|
|
207
|
+
- Recommendations: load independently and lazily; they must not delay product
|
|
208
|
+
title, price, selected variant or primary media.
|
|
209
|
+
- Shipping policy and public configuration: read once in the root data layer and
|
|
210
|
+
share the result.
|
|
211
|
+
|
|
212
|
+
### Shared website bootstrap
|
|
213
|
+
|
|
214
|
+
The public configuration is the canonical bootstrap shared by buyer accounts,
|
|
215
|
+
marketing and the SDK client. A static or server-rendered storefront should
|
|
216
|
+
fetch it once, cache it for the build/render window and pass its context into
|
|
217
|
+
the root provider:
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
import {
|
|
221
|
+
getWebsiteContextFromPublicConfig,
|
|
222
|
+
getWebsitePublicConfig,
|
|
223
|
+
} from "@bacano/sdk";
|
|
224
|
+
import { BacanoMarketingProvider, BacanoProvider } from "@bacano/sdk/react";
|
|
225
|
+
|
|
226
|
+
const publicConfig = await getWebsitePublicConfig({
|
|
227
|
+
apiUrl,
|
|
228
|
+
websiteSlug,
|
|
229
|
+
origin: canonicalOrigin,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
<BacanoProvider
|
|
233
|
+
config={{
|
|
234
|
+
apiUrl,
|
|
235
|
+
websiteSlug,
|
|
236
|
+
initialContext: getWebsiteContextFromPublicConfig(publicConfig),
|
|
237
|
+
}}
|
|
238
|
+
>
|
|
239
|
+
<BacanoMarketingProvider
|
|
240
|
+
publicConfig={publicConfig}
|
|
241
|
+
scriptLoadStrategy="idle"
|
|
242
|
+
>
|
|
243
|
+
{children}
|
|
244
|
+
</BacanoMarketingProvider>
|
|
245
|
+
</BacanoProvider>;
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`scriptLoadStrategy="idle"` prepares the GA4, Meta, and Clarity command queues
|
|
249
|
+
immediately, so events are not lost, but postpones downloading their vendor
|
|
250
|
+
scripts until browser idle time or the first interaction. The default remains
|
|
251
|
+
`"immediate"` for backward compatibility.
|
|
252
|
+
|
|
253
|
+
If the storefront refreshes public configuration in the browser, it must keep
|
|
254
|
+
the static bootstrap while the refresh is pending and only replace it when the
|
|
255
|
+
payload actually changes. Configuration failure must not blank the storefront.
|
|
256
|
+
|
|
257
|
+
Mount third-party authentication only on routes that require it, such as
|
|
258
|
+
checkout, sign-in and customer account pages. Public catalog and product pages
|
|
259
|
+
must not download an authentication runtime merely because buyer accounts are
|
|
260
|
+
enabled for the website.
|
|
261
|
+
|
|
262
|
+
## Runtime traffic audit
|
|
263
|
+
|
|
264
|
+
A Static First review is incomplete without a real browser trace. Capture a
|
|
265
|
+
fresh direct load separately from internal navigation, catalog interaction and
|
|
266
|
+
product navigation. For every GraphQL request record its `operationName`,
|
|
267
|
+
variables, initiator, phase and status.
|
|
268
|
+
|
|
269
|
+
The report must distinguish:
|
|
270
|
+
|
|
271
|
+
- data already present in exported HTML or the static bootstrap;
|
|
272
|
+
- volatile price, availability and purchase-limit refreshes;
|
|
273
|
+
- structural catalog requests caused by an explicit filter or pagination;
|
|
274
|
+
- duplicated operations with equivalent variables and no intervening
|
|
275
|
+
invalidation or user action.
|
|
276
|
+
|
|
277
|
+
The initial public route must not download full products already represented by
|
|
278
|
+
the bootstrap. Live-state requests must group and deduplicate variant IDs,
|
|
279
|
+
respect the documented batch limit and keep a failure-safe cache. A documented
|
|
280
|
+
revalidation is not a duplicate, but its reason and ownership must be visible in
|
|
281
|
+
the trace.
|
|
282
|
+
|
|
283
|
+
Also record the number of `BacanoClient` and `BacanoProvider` instances and
|
|
284
|
+
confirm that server/build `public-config` becomes the provider's
|
|
285
|
+
`initialContext`. If a browser trace cannot be captured, mark this control as
|
|
286
|
+
not verified instead of inferring it from source code.
|
|
287
|
+
|
|
288
|
+
## Acceptance checklist for storefront agents
|
|
289
|
+
|
|
290
|
+
- [ ] View source contains real product cards/details, not only skeletons.
|
|
291
|
+
- [ ] Routes, sitemap, metadata and JSON-LD reuse the same build bootstrap.
|
|
292
|
+
- [ ] No direct GraphQL and no per-product build fetch loop.
|
|
293
|
+
- [ ] One `BacanoProvider` and no duplicated website/config requests.
|
|
294
|
+
- [ ] The provider receives `initialContext` from the shared public config.
|
|
295
|
+
- [ ] Marketing commands queue before scripts and idle loading loses no events.
|
|
296
|
+
- [ ] Authentication vendor scripts are absent from public catalog routes.
|
|
297
|
+
- [ ] `srcset` and `sizes` are present on catalog and product media.
|
|
298
|
+
- [ ] Live refresh is batched, debounced and failure-safe.
|
|
299
|
+
- [ ] Recommendations and below-fold media do not block the primary content.
|
|
300
|
+
- [ ] Mobile and desktop Lighthouse runs show no material layout shift.
|
|
301
|
+
- [ ] A production trace confirms no duplicated catalog, shipping or website
|
|
302
|
+
bootstrap calls during the first navigation.
|
|
303
|
+
- [ ] The trace lists operation names and separates static, live and
|
|
304
|
+
interaction-driven data.
|