@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,499 @@
1
+ # Bacano Storefront Content Preview
2
+
3
+ Official guide for `@bacano/sdk/react/preview`, available since SDK `1.0.5`.
4
+
5
+ ## Problem solved
6
+
7
+ Static storefronts should keep their public catalog and editorial pages frozen
8
+ at build time for SEO, speed, reliability, and predictable hosting costs. That
9
+ also means a published Content change is not visible on the public page until
10
+ the storefront is rebuilt and deployed.
11
+
12
+ The preview entrypoint provides a separate client-side route that reads the
13
+ current **published** Bacano Content on demand and renders it with the
14
+ storefront's own production components:
15
+
16
+ ```text
17
+ Public route: Bacano Content -> Next build -> static HTML
18
+ Preview route: Browser -> @bacano/sdk -> current published Content
19
+ ```
20
+
21
+ Preview V1 is not a draft bypass and is not a realtime subscription. An editor
22
+ publishes the entry and then opens or refreshes the preview without rebuilding
23
+ the storefront.
24
+
25
+ ## Architectural contract
26
+
27
+ ### Bacano SDK owns
28
+
29
+ - Loading, ready, and safe error states.
30
+ - Per-route in-memory cache and simultaneous-request deduplication.
31
+ - Protection against stale responses after navigation or unmount.
32
+ - Forced refresh.
33
+ - Expanded state and four toolbar positions.
34
+ - `sessionStorage` persistence isolated by website slug.
35
+ - The official responsive and accessible toolbar.
36
+ - Neutral route normalization and internal-link interception.
37
+ - A preview-mode signal.
38
+ - A marketing no-op boundary inside the preview tree.
39
+ - SSR-safe module evaluation.
40
+
41
+ ### The storefront owns
42
+
43
+ - The single preview route, for example `/vista-previa/`.
44
+ - The route map and routes that are safe to preview.
45
+ - The loader that combines Content keys for each route.
46
+ - Mapping Bacano Content to storefront view models.
47
+ - Rendering with the same visual components used by production.
48
+ - Translating preview navigation to its router or query-string convention.
49
+ - `noindex` metadata and sitemap exclusion.
50
+ - Hiding or disabling popups, automatic chat, capture banners, cart drawers,
51
+ checkout links, and other non-editorial features outside the preview tree.
52
+
53
+ The SDK does not know whether `/` needs one Content key or six, and it does not
54
+ render Home, About, Blog, Product, Catalog, or Checkout pages.
55
+
56
+ ## Packaging
57
+
58
+ Import preview only from its dedicated entrypoint:
59
+
60
+ ```tsx
61
+ import {
62
+ BacanoPreviewShell,
63
+ interceptPreviewLink,
64
+ type PreviewRouteLoader,
65
+ } from "@bacano/sdk/react/preview";
66
+ ```
67
+
68
+ It is a separate ESM/CJS entrypoint and is not re-exported by
69
+ `@bacano/sdk/react`. It has no Next.js or Tailwind dependency. The official
70
+ toolbar carries self-contained styles and does not require a stylesheet import.
71
+ Both React entrypoints share the same Bacano client and marketing contexts, so
72
+ preview must remain under the storefront's single `BacanoProvider`.
73
+
74
+ React `>=18` remains the only peer dependency.
75
+
76
+ ## Required provider hierarchy
77
+
78
+ Mount exactly one `BacanoProvider` for the storefront, independently of whether
79
+ buyer accounts are enabled:
80
+
81
+ ```tsx
82
+ import { BacanoProvider } from "@bacano/sdk/react";
83
+
84
+ export function StorefrontProviders({
85
+ children,
86
+ }: {
87
+ children: React.ReactNode;
88
+ }) {
89
+ return (
90
+ <BacanoProvider
91
+ config={{
92
+ apiUrl: process.env.NEXT_PUBLIC_BACANO_API_URL!,
93
+ websiteSlug: process.env.NEXT_PUBLIC_BACANO_WEBSITE_SLUG!,
94
+ }}
95
+ >
96
+ {children}
97
+ </BacanoProvider>
98
+ );
99
+ }
100
+ ```
101
+
102
+ `BacanoPreviewProvider` consumes this existing client. Do not create another
103
+ client inside the preview loader and do not mount a second `BacanoProvider`.
104
+
105
+ If Clerk supplies buyer tokens, keep its token callback in this same provider.
106
+ The presence or absence of Clerk must not determine whether the public Bacano
107
+ client exists.
108
+
109
+ ## Public API
110
+
111
+ ### `BacanoPreviewProvider<TDocument>`
112
+
113
+ Required props:
114
+
115
+ | Prop | Purpose |
116
+ | ------------- | ------------------------------------------------------- |
117
+ | `websiteSlug` | Isolates persisted toolbar state between stores. |
118
+ | `route` | Current controlled storefront route. |
119
+ | `loadRoute` | Storefront loader receiving `{ route, force, client }`. |
120
+ | `navigate` | Updates the preview URL using the storefront router. |
121
+
122
+ Optional props:
123
+
124
+ | Prop | Default | Purpose |
125
+ | -------------------- | ----------------------- | -------------------------------------------- |
126
+ | `normalizeRoute` | `normalizePreviewRoute` | Canonicalizes route input. |
127
+ | `isPreviewableRoute` | all normalized routes | Rejects private or transactional routes. |
128
+ | `initialExpanded` | `true` | Initial toolbar visibility before hydration. |
129
+ | `initialPosition` | `top-right` | Initial toolbar corner. |
130
+ | `cacheMaxEntries` | `50` | Maximum cached route documents per provider. |
131
+
132
+ Loader contract:
133
+
134
+ ```ts
135
+ type PreviewRouteLoader<TDocument> = (context: {
136
+ route: string;
137
+ force: boolean;
138
+ client: BacanoClient;
139
+ }) => Promise<TDocument>;
140
+ ```
141
+
142
+ A `null` or `undefined` result becomes the safe `EMPTY_DOCUMENT` state. Loader
143
+ errors are not exposed verbatim in the public UI.
144
+
145
+ ### `useBacanoPreview<TDocument>()`
146
+
147
+ Returns:
148
+
149
+ ```ts
150
+ {
151
+ isPreview: true;
152
+ websiteSlug: string;
153
+ route: string;
154
+ status: "idle" | "loading" | "ready" | "error";
155
+ document: TDocument | null;
156
+ error: { code: string; message: string } | null;
157
+ refreshedAt: Date | null;
158
+ isExpanded: boolean;
159
+ position: "top-left" | "top-right" | "bottom-left" | "bottom-right";
160
+ refresh(): Promise<void>;
161
+ navigate(route: string): void;
162
+ setExpanded(expanded: boolean): void;
163
+ setPosition(position): void;
164
+ }
165
+ ```
166
+
167
+ ### `BacanoPreviewToolbar`
168
+
169
+ The optional tokens are deliberately limited:
170
+
171
+ ```tsx
172
+ <BacanoPreviewToolbar
173
+ websiteName="My store"
174
+ logo={<img src="/logo.svg" alt="" />}
175
+ primaryColor="#172033"
176
+ accentColor="#2f80ed"
177
+ backgroundColor="#ffffff"
178
+ locale="es-CO"
179
+ zIndex={2147483000}
180
+ suggestedRoutes={["/", "/about", "/blog"]}
181
+ />
182
+ ```
183
+
184
+ The toolbar provides route input, refresh, collapse/expand, last refresh time,
185
+ safe status messages, keyboard focus, `aria-live`, responsive layout, reduced
186
+ motion support, and four positions.
187
+
188
+ ### `BacanoPreviewShell<TDocument>`
189
+
190
+ Convenience composition of the provider and toolbar:
191
+
192
+ ```tsx
193
+ <BacanoPreviewShell {...providerProps} toolbar={toolbarProps}>
194
+ {(preview) => renderPreviewState(preview)}
195
+ </BacanoPreviewShell>
196
+ ```
197
+
198
+ Pass `toolbar={false}` only when the storefront supplies another accessible
199
+ control surface.
200
+
201
+ ## Minimal Next.js static-export example
202
+
203
+ The SDK contains no Next.js imports. This adapter belongs to the storefront.
204
+
205
+ ```tsx
206
+ // app/vista-previa/page.tsx
207
+ import type { Metadata } from "next";
208
+ import { Suspense } from "react";
209
+ import { PreviewClient } from "./PreviewClient";
210
+
211
+ export const metadata: Metadata = {
212
+ title: "Vista previa del sitio",
213
+ robots: { index: false, follow: false },
214
+ };
215
+
216
+ export default function PreviewPage() {
217
+ return (
218
+ <Suspense fallback={<p>Cargando vista previa...</p>}>
219
+ <PreviewClient />
220
+ </Suspense>
221
+ );
222
+ }
223
+ ```
224
+
225
+ ```tsx
226
+ // app/vista-previa/PreviewClient.tsx
227
+ "use client";
228
+
229
+ import { useSearchParams } from "next/navigation";
230
+ import {
231
+ BacanoPreviewShell,
232
+ interceptPreviewLink,
233
+ normalizePreviewRoute,
234
+ type PreviewRouteLoader,
235
+ } from "@bacano/sdk/react/preview";
236
+
237
+ type PreviewDocument =
238
+ | { kind: "home"; hero: unknown; story: unknown }
239
+ | { kind: "about"; content: unknown }
240
+ | { kind: "unsupported"; route: string };
241
+
242
+ const previewable = new Set(["/", "/about"]);
243
+
244
+ const loadRoute: PreviewRouteLoader<PreviewDocument> = async ({
245
+ route,
246
+ client,
247
+ }) => {
248
+ if (route === "/") {
249
+ const [hero, story] = await Promise.all([
250
+ client.content.getByKey("home-hero"),
251
+ client.content.getByKey("home-story"),
252
+ ]);
253
+ return { kind: "home", hero, story };
254
+ }
255
+
256
+ if (route === "/about") {
257
+ return {
258
+ kind: "about",
259
+ content: await client.content.getByKey("about"),
260
+ };
261
+ }
262
+
263
+ return { kind: "unsupported", route };
264
+ };
265
+
266
+ export function PreviewClient() {
267
+ const searchParams = useSearchParams();
268
+ const route = normalizePreviewRoute(searchParams.get("route"));
269
+
270
+ return (
271
+ <BacanoPreviewShell
272
+ websiteSlug={process.env.NEXT_PUBLIC_BACANO_WEBSITE_SLUG!}
273
+ route={route}
274
+ loadRoute={loadRoute}
275
+ isPreviewableRoute={(nextRoute) => previewable.has(nextRoute)}
276
+ navigate={(nextRoute) => {
277
+ window.location.assign(
278
+ `/vista-previa/?route=${encodeURIComponent(nextRoute)}`,
279
+ );
280
+ }}
281
+ toolbar={{
282
+ websiteName: "My store",
283
+ suggestedRoutes: Array.from(previewable),
284
+ }}
285
+ >
286
+ {(preview) => {
287
+ if (preview.status === "loading" && !preview.document) {
288
+ return <PreviewSkeleton />;
289
+ }
290
+ if (preview.status === "error" && !preview.document) {
291
+ return <PreviewError onRetry={preview.refresh} />;
292
+ }
293
+ if (!preview.document) return null;
294
+
295
+ return (
296
+ <div
297
+ onClickCapture={(event) =>
298
+ interceptPreviewLink(event, {
299
+ navigate: preview.navigate,
300
+ isPreviewableRoute: (nextRoute) => previewable.has(nextRoute),
301
+ })
302
+ }
303
+ >
304
+ <ProductionRenderer document={preview.document} />
305
+ </div>
306
+ );
307
+ }}
308
+ </BacanoPreviewShell>
309
+ );
310
+ }
311
+ ```
312
+
313
+ The example intentionally leaves `PreviewSkeleton`, `PreviewError`, and
314
+ `ProductionRenderer` to the storefront so the page retains its visual language.
315
+
316
+ ## Route map guidance
317
+
318
+ Keep a finite allowlist. Typical V1 routes include:
319
+
320
+ - Home.
321
+ - About and other editorial pages.
322
+ - Blog index and published articles.
323
+ - Content-driven catalog landing pages.
324
+ - Published legal pages.
325
+
326
+ Do not preview in V1:
327
+
328
+ - Checkout or payment response.
329
+ - Account creation, login, or private account pages.
330
+ - Private orders.
331
+ - Gateway sessions.
332
+ - Any route whose primary purpose is transactional.
333
+
334
+ Product pages can be added later if the storefront defines an explicit loader
335
+ and non-transactional renderer. The SDK does not enable them automatically.
336
+
337
+ ## Internal-link interception
338
+
339
+ `interceptPreviewLink()` only captures plain primary clicks on same-origin
340
+ links. It preserves:
341
+
342
+ - Ctrl/Cmd click.
343
+ - Shift and Alt click.
344
+ - External origins.
345
+ - `target="_blank"`.
346
+ - `download` links.
347
+ - Routes rejected by `isPreviewableRoute`.
348
+
349
+ The callback receives the normalized public route. The storefront decides how
350
+ to encode it into `/vista-previa/?route=...`.
351
+
352
+ ## Marketing and commercial UI isolation
353
+
354
+ `BacanoPreviewProvider` overrides `useMarketing()` with a synchronous no-op in
355
+ its subtree. This prevents GA4, Meta, and Clarity browser events from preview renderers,
356
+ even if the root storefront marketing runtime is active.
357
+
358
+ `useBacanoPreviewMode()` is also available to components placed inside the
359
+ preview boundary:
360
+
361
+ ```tsx
362
+ const isPreview = useBacanoPreviewMode();
363
+ if (isPreview) return null;
364
+ return <CommercialPopup />;
365
+ ```
366
+
367
+ Components rendered above or beside the preview provider cannot read a child
368
+ context. The storefront must therefore hide or disable root-level commercial
369
+ elements on its preview route, including:
370
+
371
+ - Marketing popups and lead banners.
372
+ - Automatic chat and WhatsApp launchers.
373
+ - Cart drawers and checkout calls to action.
374
+ - Independently mounted analytics providers.
375
+
376
+ When `BacanoMarketingProvider` is nested inside a preview-mode boundary, it also
377
+ recognizes the signal and remains a no-op.
378
+
379
+ Server-side conversion events remain protected by keeping all transactional
380
+ routes and actions outside preview. A browser preview flag is not an
381
+ authorization mechanism and must never be sent to bypass commercial rules.
382
+
383
+ ## Security and privacy
384
+
385
+ V1 calls only the existing public Content methods:
386
+
387
+ - `client.content.getByKey()`
388
+ - `client.content.getByCategory()`
389
+ - `client.content.getByTag()`
390
+
391
+ Those contracts return current published content. There is no `includeDrafts`,
392
+ GraphQL bypass, alternate endpoint, admin secret, or permission change.
393
+
394
+ The route remains publicly reachable, so it must show only information that is
395
+ already public. Keep `noindex`, exclude it from sitemap, and do not place tokens,
396
+ customer data, payment data, or secret identifiers in the preview query string.
397
+
398
+ A future draft preview requires a Bacano-signed session containing website,
399
+ user, scopes, allowed routes, expiration, revocation, and audit information.
400
+ Never implement drafts using `?includeDrafts=true`.
401
+
402
+ ## Static production guarantees
403
+
404
+ When the storefront follows this contract:
405
+
406
+ - Public pages keep their build-time Content snapshots.
407
+ - `output: "export"` remains valid.
408
+ - `generateStaticParams`, sitemap, and public metadata do not change.
409
+ - The preview entrypoint is imported only by its one route.
410
+ - Preview API errors affect only the preview route.
411
+ - A temporary Bacano outage while someone uses preview does not alter the
412
+ already deployed public storefront.
413
+
414
+ The build still depends on whatever Bacano data the public static pages request
415
+ at build time. Preview does not change or replace that build contract.
416
+
417
+ ## Validated reference implementation: Baby Rex
418
+
419
+ Baby Rex validated the official integration with `@bacano/sdk` `1.0.5` on
420
+ 2026-08-06. It uses one global `BacanoProvider`; `BacanoPreviewShell` consumes
421
+ that initialized client and the loader receives `{ route, force, client }`.
422
+
423
+ The validated static production build generated all `417/417` pages. TypeScript,
424
+ lint, unit tests, E2E, preview navigation, cache, refresh, toolbar behavior, and
425
+ the `idle -> loading -> ready` transition passed. No marketing event was emitted
426
+ inside preview.
427
+
428
+ The implementation removed these generic storefront responsibilities:
429
+
430
+ - Local `PreviewToolbar.tsx`.
431
+ - Local expanded/position/sessionStorage state.
432
+ - Local loading/error/refreshed-at controller state.
433
+ - Local request-promise cache and forced-refresh orchestration.
434
+ - Generic internal-link interception.
435
+ - Pathname-based marketing suppression inside preview renderers once the SDK
436
+ boundary covers them.
437
+
438
+ Baby Rex retained these storefront-specific responsibilities:
439
+
440
+ - `/vista-previa/page.tsx` and its `noindex` metadata.
441
+ - Sitemap exclusion.
442
+ - `PreviewDocument` union.
443
+ - Route allowlist and suggested routes.
444
+ - `loadPreviewDocument()` mappings for Home, About, Blog, legal pages, and
445
+ content-driven catalog sections.
446
+ - `PreviewDocumentView` and all production renderers.
447
+ - Storefront-specific skeleton, missing-content, and unsupported-route views.
448
+ - The Next.js adapter that writes `?ruta={encodedRoute}`.
449
+
450
+ Baby Rex is the implementation reference for integration shape, not a source to
451
+ copy blindly. Other storefronts keep their own route maps, Content keys,
452
+ mappers, and visual components.
453
+
454
+ ## Preview audit contract
455
+
456
+ An audit must verify both the SDK lifecycle and the storefront result model.
457
+ The SDK lifecycle is `idle`, `loading`, `ready` or `error`; `missing` and
458
+ `unsupported` are storefront document/view outcomes and must not be presented
459
+ as extra SDK statuses.
460
+
461
+ At minimum, automate or capture evidence for:
462
+
463
+ - a direct request to the single preview route returning `noindex, nofollow`;
464
+ - the preview route being absent from every sitemap;
465
+ - reuse of the global `BacanoProvider` and initialized `BacanoClient`;
466
+ - a finite route allowlist that rejects account, checkout, payment and order
467
+ routes;
468
+ - absence of GA4, Meta, Clarity and other effective marketing scripts, requests
469
+ and events on a direct preview load;
470
+ - `idle -> loading -> ready`, safe error, missing and unsupported rendering;
471
+ - cache reuse, forced refresh and internal navigation;
472
+ - unchanged static HTML, route generation and sitemap for public pages.
473
+
474
+ The final report must name the requests observed in preview and distinguish an
475
+ unexecuted browser check from a passing zero-request assertion.
476
+
477
+ ## Short checklist for agents
478
+
479
+ - [ ] Use `@bacano/sdk` `1.0.5` or newer.
480
+ - [ ] Mount exactly one global `BacanoProvider`.
481
+ - [ ] Place `BacanoPreviewShell` below that provider; never create a preview client.
482
+ - [ ] Load only published Content through the `client` supplied to `loadRoute`.
483
+ - [ ] Keep route allowlists, Content keys, mappers, and renderers in the storefront.
484
+ - [ ] Keep preview `noindex`, outside the sitemap, and free of transactional routes.
485
+ - [ ] Hide root-level commercial UI and verify zero marketing events in preview.
486
+ - [ ] Test idle, loading, ready, safe error, refresh, cache, and navigation states.
487
+ - [ ] Test storefront-specific missing and unsupported outcomes.
488
+ - [ ] Capture a direct preview trace proving zero effective marketing traffic.
489
+ - [ ] Verify static public pages and their build output remain unchanged.
490
+ - [ ] Run typecheck, lint, unit tests, E2E, and the complete production build.
491
+
492
+ ## V1 limitations
493
+
494
+ - Published Content only.
495
+ - Manual refresh; no WebSocket, polling, or subscriptions.
496
+ - In-memory document cache lasts for the mounted provider session.
497
+ - Toolbar preferences use browser `sessionStorage`.
498
+ - The storefront owns all route-specific loaders and renderers.
499
+ - Transactional and private pages are intentionally excluded.
@@ -0,0 +1,127 @@
1
+ # Fichas técnicas de producto en storefronts
2
+
3
+ Este documento define el contrato público de `@bacano/sdk` para mostrar y
4
+ filtrar la información técnica creada por cada comercio en Bacano Content.
5
+
6
+ La capacidad es independiente de los atributos y variantes comerciales. Las
7
+ variantes continúan siendo la fuente de combinaciones, SKU, precio e
8
+ inventario; una ficha técnica no debe usarse para recrearlas ni modificarlas.
9
+
10
+ ## Lectura en detalle y builds estáticos
11
+
12
+ `Product.specification` contiene la ficha técnica efectiva del producto o
13
+ `null` cuando no existe una ficha pública con valores. Está disponible en:
14
+
15
+ - `client.catalog.getProduct()`;
16
+ - `client.catalog.getProductBySlug()`;
17
+ - los productos de `client.catalog.getStaticCatalogBootstrap()`;
18
+ - los productos devueltos por `client.catalog.getCatalogPage()`.
19
+
20
+ Las tiendas estáticas deben consumir el valor que llega en el bootstrap. Un
21
+ cambio público en la definición o los valores requiere **Publicar cambios** y
22
+ un nuevo build del storefront. Stock, precios y descuentos conservan su flujo
23
+ dinámico independiente.
24
+
25
+ ```tsx
26
+ import type { ProductSpecification } from "@bacano/sdk";
27
+
28
+ export function ProductTechnicalSpecification({
29
+ specification,
30
+ }: {
31
+ specification: ProductSpecification | null;
32
+ }) {
33
+ if (!specification) return null;
34
+
35
+ return (
36
+ <section aria-labelledby="technical-specification-title">
37
+ <h2 id="technical-specification-title">Información adicional</h2>
38
+ {specification.sections.map((section) => (
39
+ <div key={section.id ?? section.key}>
40
+ <h3>{section.title}</h3>
41
+ {section.description ? <p>{section.description}</p> : null}
42
+ <dl>
43
+ {section.fields.map((field) => (
44
+ <div key={field.id}>
45
+ <dt>{field.label}</dt>
46
+ <dd>{formatSpecificationValue(field.value)}</dd>
47
+ </div>
48
+ ))}
49
+ </dl>
50
+ </div>
51
+ ))}
52
+ </section>
53
+ );
54
+ }
55
+
56
+ function formatSpecificationValue(value: unknown): string {
57
+ if (typeof value === "boolean") return value ? "Sí" : "No";
58
+ if (typeof value === "string" || typeof value === "number") {
59
+ return String(value);
60
+ }
61
+ if (Array.isArray(value)) {
62
+ return value.map(formatSpecificationValue).join(", ");
63
+ }
64
+ if (value && typeof value === "object") {
65
+ if ("label" in value && typeof value.label === "string") {
66
+ return value.label;
67
+ }
68
+ if ("unit" in value && "value" in value) {
69
+ const measure = value as {
70
+ value: number;
71
+ unit: { symbol: string };
72
+ };
73
+ return `${measure.value} ${measure.unit.symbol}`;
74
+ }
75
+ }
76
+ return "";
77
+ }
78
+ ```
79
+
80
+ Los campos vacíos o internos ya son omitidos por Bacano. El storefront recibe
81
+ la organización por secciones y puede reproducirla o aplicar su propio diseño.
82
+ No debe consultar Hasura, interpretar el JSON editable ni reconstruir unidades
83
+ u opciones por su cuenta.
84
+
85
+ ## Facetas técnicas
86
+
87
+ Solicitar `filters: true` en `getCatalogPage()` devuelve
88
+ `result.filters.technical`. Cada faceta expone un `fieldId` estable, tipo,
89
+ rango, unidad, opciones y conteos relevantes para los productos públicos y
90
+ vendibles del resultado.
91
+
92
+ ```ts
93
+ const page = await client.catalog.getCatalogPage({
94
+ categoryListKey: "categorias",
95
+ attributeListKey: "atributos",
96
+ categorySlug: "vehiculos-electricos",
97
+ technicalByField: {
98
+ "FIELD_UUID_AUTONOMIA": { min: 40, max: 120 },
99
+ "FIELD_UUID_BATERIA": { optionIds: ["OPTION_UUID_LITIO"] },
100
+ },
101
+ include: { products: true, filters: true },
102
+ });
103
+ ```
104
+
105
+ Reglas del filtro:
106
+
107
+ - los valores seleccionados dentro de una misma faceta se combinan con OR;
108
+ - las facetas diferentes se combinan con AND;
109
+ - número y medida aceptan `min` y `max`;
110
+ - fecha acepta `dateFrom` y `dateTo`;
111
+ - booleano acepta `booleanValues`;
112
+ - lista simple o múltiple acepta `optionIds`;
113
+ - el storefront solo debe enviar IDs publicados por `filters.technical`.
114
+
115
+ Bacano rechaza como filtro cualquier campo interno, oculto o no marcado como
116
+ filtrable aunque se intente enviar su UUID manualmente.
117
+
118
+ ## Reglas de integración
119
+
120
+ - No crear GraphQL directo ni consultar el JSON flexible de Content.
121
+ - No persistir una segunda copia de la ficha en el storefront.
122
+ - No convertir fichas técnicas en atributos o variantes.
123
+ - No inventar facetas para campos que Bacano no publica como filtrables.
124
+ - Conservar el orden de secciones, campos y opciones entregado por el SDK.
125
+ - Omitir por completo el bloque cuando `specification` sea `null`.
126
+ - Validar el build estático con productos con y sin ficha antes de publicar.
127
+