@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,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
|
+
|