@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.
Files changed (72) hide show
  1. package/README.md +439 -18
  2. package/STOREFRONT_CLARITY.md +140 -0
  3. package/STOREFRONT_CONSTRUCTION.md +105 -0
  4. package/STOREFRONT_LEADS.md +130 -0
  5. package/STOREFRONT_LEGAL.md +66 -0
  6. package/STOREFRONT_MARKETING.md +291 -0
  7. package/STOREFRONT_PERFORMANCE.md +304 -0
  8. package/STOREFRONT_PREVIEW.md +499 -0
  9. package/STOREFRONT_PRODUCT_SPECIFICATIONS.md +127 -0
  10. package/STOREFRONT_VOLUME_PRICING.md +131 -0
  11. package/client.d.ts +8 -1
  12. package/errors.d.ts +9 -1
  13. package/graphql.cjs +266 -3
  14. package/graphql.d.ts +2 -2
  15. package/graphql.js +266 -3
  16. package/index.cjs +2035 -255
  17. package/index.d.ts +8 -6
  18. package/index.js +2035 -255
  19. package/marketing/attribution.d.ts +53 -0
  20. package/marketing/clarity.d.ts +3 -0
  21. package/marketing/domain.d.ts +1 -0
  22. package/marketing/ga4.d.ts +2 -0
  23. package/marketing/index.d.ts +5 -0
  24. package/marketing/runtime.d.ts +2 -0
  25. package/marketing/sanitize.d.ts +2 -0
  26. package/marketing/types.d.ts +111 -0
  27. package/media.d.ts +45 -2
  28. package/modules/auth.d.ts +3 -2
  29. package/modules/availability.d.ts +2 -2
  30. package/modules/cart-errors.d.ts +2 -0
  31. package/modules/cart-session.d.ts +2 -2
  32. package/modules/cart.d.ts +4 -3
  33. package/modules/catalog.d.ts +15 -3
  34. package/modules/content.d.ts +2 -2
  35. package/modules/marketing.d.ts +5 -0
  36. package/modules/orders.d.ts +2 -2
  37. package/modules/profile.d.ts +1 -1
  38. package/modules/static-catalog.d.ts +13 -0
  39. package/modules/website.d.ts +7 -1
  40. package/package.json +14 -3
  41. package/react/context-registry.d.ts +9 -0
  42. package/react/context.d.ts +1 -1
  43. package/react/hooks.d.ts +24 -2
  44. package/react/marketing-context.d.ts +3 -0
  45. package/react/marketing-provider.d.ts +10 -0
  46. package/react/preview/cache.d.ts +23 -0
  47. package/react/preview/context.d.ts +5 -0
  48. package/react/preview/navigation.d.ts +9 -0
  49. package/react/preview/provider.d.ts +3 -0
  50. package/react/preview/shell.d.ts +3 -0
  51. package/react/preview/toolbar.d.ts +3 -0
  52. package/react/preview/types.d.ts +60 -0
  53. package/react/preview.cjs +742 -0
  54. package/react/preview.d.ts +14 -0
  55. package/react/preview.js +730 -0
  56. package/react/website-gate.d.ts +22 -0
  57. package/react.cjs +3893 -1795
  58. package/react.d.ts +8 -4
  59. package/react.js +3891 -1789
  60. package/skills/bacano-web/SKILL.md +116 -0
  61. package/skills/bacano-web/agents/openai.yaml +4 -0
  62. package/skills/bacano-web/references/architecture-runtime.md +109 -0
  63. package/skills/bacano-web/references/common.md +92 -0
  64. package/skills/bacano-web/references/informational.md +39 -0
  65. package/skills/bacano-web/references/report.md +70 -0
  66. package/skills/bacano-web/references/storefront.md +116 -0
  67. package/skills/bacano-web/scripts/audit-http.mjs +284 -0
  68. package/skills/bacano-web/scripts/cli.mjs +222 -0
  69. package/skills/bacano-web/scripts/postinstall.mjs +8 -0
  70. package/transport/documents.d.ts +23 -18
  71. package/transport/graphql.d.ts +13 -1
  72. 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.