@bacano/sdk 1.1.17 → 1.1.19
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/CONTENT_TYPE_MANAGEMENT.md +80 -2
- package/README.md +11 -3
- package/STOREFRONT_CONTENT_LOCATOR.md +111 -0
- package/STOREFRONT_FORMS.md +75 -5
- package/STOREFRONT_LEADS.md +1 -1
- package/STOREFRONT_PREVIEW.md +21 -8
- package/STOREFRONT_VOLUME_PRICING.md +1 -1
- package/content-entry-locator.d.ts +16 -0
- package/content-management-types.d.ts +4 -3
- package/content-management.cjs +37 -0
- package/content-management.d.ts +1 -0
- package/content-management.js +35 -0
- package/form-management-types.d.ts +62 -0
- package/form-management.d.ts +2 -0
- package/index.cjs +23 -0
- package/index.d.ts +2 -0
- package/index.js +23 -0
- package/package.json +1 -1
- package/react/form-field-control.d.ts +1 -0
- package/react/forms.cjs +14 -4
- package/react/forms.js +15 -4
- package/skills/bacano-figma-web/SKILL.md +9 -1
- package/skills/bacano-figma-web/references/bacano-mapping.md +5 -3
- package/skills/bacano-figma-web/references/content-workflow.md +18 -1
- package/skills/bacano-figma-web/references/implementation-quality.md +12 -0
- package/skills/bacano-web/SKILL.md +4 -1
- package/skills/bacano-web/references/common.md +7 -0
- package/skills/bacano-web/references/forms.md +32 -2
- package/types.d.ts +5 -1
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
`@bacano/sdk/content-management` permite que una herramienta administrativa o
|
|
4
4
|
un agente autorizado consulte y mantenga **tipos, componentes, grupos, listas
|
|
5
|
-
de opciones, entradas e imágenes** de Bacano Content
|
|
5
|
+
de opciones, entradas e imágenes** de Bacano Content, y **formularios web**.
|
|
6
|
+
Cada capacidad requiere
|
|
6
7
|
un permiso explícito. El acceso no autoriza productos, ventas, clientes,
|
|
7
8
|
configuración general, GraphQL, SQL ni acceso directo a Storage.
|
|
8
9
|
|
|
@@ -12,7 +13,7 @@ dentro del bundle del sitio web.
|
|
|
12
13
|
|
|
13
14
|
## Activar un acceso temporal
|
|
14
15
|
|
|
15
|
-
1. En Bacano, abre **Sitios web → Contenido → Tipos** o **
|
|
16
|
+
1. En Bacano, abre **Sitios web → Contenido → Tipos**, **Componentes** o **Formularios**.
|
|
16
17
|
2. Selecciona **Conectar agente**.
|
|
17
18
|
3. Elige el alcance, 15, 30 o 60 minutos y genera el acceso.
|
|
18
19
|
4. Copia el token al entorno local del agente.
|
|
@@ -32,6 +33,15 @@ Scopes disponibles:
|
|
|
32
33
|
también `content_entries:manage`.
|
|
33
34
|
- `content_media:upload`: consultar imágenes de la empresa y cargar JPEG, PNG o
|
|
34
35
|
WebP. Requiere también `content_entries:manage`; no permite borrar archivos.
|
|
36
|
+
- `forms:manage`: consultar sitios y listas disponibles para formularios,
|
|
37
|
+
inventariar formularios y crear o reemplazar sus borradores.
|
|
38
|
+
- `forms:publish`: publicar, pausar, reactivar o archivar formularios. Requiere
|
|
39
|
+
`forms:manage`. También se exige para cambiar notificaciones de formularios
|
|
40
|
+
que ya fueron publicados, porque afectan envíos en curso.
|
|
41
|
+
|
|
42
|
+
Los permisos de Content no conceden acceso a formularios ni viceversa. Los
|
|
43
|
+
tokens existentes no se amplían; solicita uno nuevo con el alcance necesario.
|
|
44
|
+
Estos permisos no permiten leer respuestas, datos personales, leads ni métricas.
|
|
35
45
|
|
|
36
46
|
No guardes el token en Git, Figma, Content, logs, variables públicas ni archivos
|
|
37
47
|
que se empaqueten con el storefront.
|
|
@@ -432,6 +442,74 @@ omitas opciones existentes por accidente en una actualización.
|
|
|
432
442
|
- Los tokens v2 no son compatibles con tokens anteriores y tienen como máximo
|
|
433
443
|
60 minutos de vigencia.
|
|
434
444
|
|
|
445
|
+
## Formularios: misma definición en el agente, el ERP y el storefront
|
|
446
|
+
|
|
447
|
+
No modeles un formulario como un tipo de Content ni mantengas una copia de sus
|
|
448
|
+
campos en el frontend. Usa el mismo cliente administrativo con `forms:manage`:
|
|
449
|
+
|
|
450
|
+
1. `getContext()` confirma empresa, scopes y vencimiento.
|
|
451
|
+
2. `getFormReferences()` devuelve los sitios de esa empresa y las Listas de
|
|
452
|
+
Content activas, con sus pares `value` / `label`.
|
|
453
|
+
3. `listForms({ websiteId, key, limit, offset })` inventaría antes de crear.
|
|
454
|
+
Las keys son únicas por sitio, no por empresa. Pagina hasta alcanzar `total`.
|
|
455
|
+
4. Presenta al usuario sitio, key, campos, acción y mensajes antes de crear.
|
|
456
|
+
5. `createForm(input)` crea exclusivamente un borrador.
|
|
457
|
+
6. `getForm(formId)` consulta la definición completa y su `updatedAt`.
|
|
458
|
+
7. `updateForm(formId, { ...input, expectedUpdatedAt })` reemplaza el borrador
|
|
459
|
+
completo. Conserva los campos que deben permanecer; no cambia key ni sitio.
|
|
460
|
+
8. Solo con aprobación humana y `forms:publish`, usa `publishForm(formId,
|
|
461
|
+
{ expectedUpdatedAt })`. Crea una versión inmutable que consume el SDK público.
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
const { websites, optionLists } = await content.getFormReferences();
|
|
465
|
+
// Elegir y confirmar con el usuario un websiteId devuelto por websites.
|
|
466
|
+
const form = await content.createForm({
|
|
467
|
+
websiteId,
|
|
468
|
+
key: "solicitar-informacion",
|
|
469
|
+
name: "Solicitar información",
|
|
470
|
+
action: "CAPTURE_LEAD",
|
|
471
|
+
definition: {
|
|
472
|
+
schemaVersion: 1,
|
|
473
|
+
submitLabel: "Enviar solicitud",
|
|
474
|
+
successMessage: "Recibimos tu solicitud.",
|
|
475
|
+
unavailableMessage: "Este formulario no está disponible.",
|
|
476
|
+
fields: [
|
|
477
|
+
{ key: "correo", label: "Correo electrónico", type: "email", required: true },
|
|
478
|
+
{
|
|
479
|
+
key: "intereses", label: "¿Qué te interesa?", type: "multi_select", required: true,
|
|
480
|
+
optionSource: { kind: "INLINE", options: [
|
|
481
|
+
{ value: "sitio_web", label: "Sitio web" },
|
|
482
|
+
{ value: "tienda_virtual", label: "Tienda virtual" },
|
|
483
|
+
] },
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
sections: [],
|
|
487
|
+
steps: [],
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
// Tras revisar y aprobar; requiere forms:publish:
|
|
491
|
+
await content.publishForm(form.id, { expectedUpdatedAt: form.updatedAt });
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Las opciones `select`, `radio` y `multi_select` usan pares `value` / `label`
|
|
495
|
+
o `optionSource: { kind: "CONTENT_LIST", listKey }`. Reutiliza únicamente una
|
|
496
|
+
lista activa de `getFormReferences()`; administrarla exige el permiso separado
|
|
497
|
+
de Content. Los tipos `email` y `phone` conservan la semántica de contacto.
|
|
498
|
+
`CAPTURE_LEAD` utiliza la resolución de identidad ya existente: no llames además
|
|
499
|
+
a la captura de campañas ni crees otro lead desde el agente.
|
|
500
|
+
|
|
501
|
+
`changeFormStatus(formId, { status, expectedUpdatedAt })` acepta `ACTIVE`,
|
|
502
|
+
`PAUSED` o `ARCHIVED`, con aprobación humana. Activar o pausar requiere una
|
|
503
|
+
versión publicada; archivar es terminal en el flujo actual. No hay borrado ni
|
|
504
|
+
restauración por este acceso.
|
|
505
|
+
|
|
506
|
+
Ante `WEBSITE_FORM_REVISION_CONFLICT` (HTTP 409), vuelve a consultar y presenta
|
|
507
|
+
las diferencias; nunca reintentes automáticamente reemplazando la revisión.
|
|
508
|
+
Editar un borrador no publica su estructura: la versión pública anterior sigue
|
|
509
|
+
vigente hasta publicar. Las respuestas y sus snapshots no se reescriben.
|
|
510
|
+
Usa `BacanoForm` o `client.forms` con la key publicada y el mismo website,
|
|
511
|
+
siguiendo `STOREFRONT_FORMS.md`. El token temporal jamás entra en ese bundle.
|
|
512
|
+
|
|
435
513
|
## Límites de esta versión
|
|
436
514
|
|
|
437
515
|
- La carga administrativa solo admite imágenes raster JPEG, PNG y WebP. SVG,
|
package/README.md
CHANGED
|
@@ -21,10 +21,10 @@ React hooks are available from `@bacano/sdk/react` and require `react >= 18`
|
|
|
21
21
|
as a peer dependency. The core client has no runtime dependencies and works
|
|
22
22
|
without React.
|
|
23
23
|
|
|
24
|
-
### Gestión de
|
|
24
|
+
### Gestión de Content y formularios para agentes
|
|
25
25
|
|
|
26
26
|
Un agente autorizado puede consultar y mantener tipos, componentes, grupos,
|
|
27
|
-
listas de opciones y
|
|
27
|
+
listas de opciones, entradas y formularios con el entrypoint administrativo
|
|
28
28
|
`@bacano/sdk/content-management`. Cada capacidad exige un scope explícito. El
|
|
29
29
|
acceso se genera temporalmente desde Bacano, está limitado a una empresa y
|
|
30
30
|
nunca debe incluirse en el storefront:
|
|
@@ -118,6 +118,14 @@ official `BacanoForm` renderer or consume the same typed definition through
|
|
|
118
118
|
`useBacanoForm()` without GraphQL, HTML stored in the database or a rebuild
|
|
119
119
|
after each publication.
|
|
120
120
|
|
|
121
|
+
Every visible region sourced from a Bacano `ContentEntry` must expose the
|
|
122
|
+
entry-level marker defined in
|
|
123
|
+
[`STOREFRONT_CONTENT_LOCATOR.md`](./STOREFRONT_CONTENT_LOCATOR.md). Use
|
|
124
|
+
`getContentEntryLocatorAttributes(entry)` on the outer section or card so an
|
|
125
|
+
authorized internal browser tool can show the entry name and open its Bacano
|
|
126
|
+
editor. This contract identifies entries only; it does not expose field-level
|
|
127
|
+
editing or grant backoffice access.
|
|
128
|
+
|
|
121
129
|
Static and hybrid stores must also follow
|
|
122
130
|
[`STOREFRONT_PERFORMANCE.md`](./STOREFRONT_PERFORMANCE.md). It defines the
|
|
123
131
|
official build bootstrap, live-state refresh, responsive media and request
|
|
@@ -213,7 +221,7 @@ The public storefront remains static. Preview V1 never exposes drafts and
|
|
|
213
221
|
automatically disables the SDK marketing runtime inside its tree. See
|
|
214
222
|
[`STOREFRONT_PREVIEW.md`](./STOREFRONT_PREVIEW.md), the canonical contract for
|
|
215
223
|
provider hierarchy, responsibilities, security, agent checklist, and the
|
|
216
|
-
validated
|
|
224
|
+
validated reference implementation.
|
|
217
225
|
|
|
218
226
|
---
|
|
219
227
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Localizador de entradas de Content
|
|
2
|
+
|
|
3
|
+
Este documento define el contrato público V1 que permite reconocer desde el
|
|
4
|
+
navegador qué entrada de Bacano Content produce una región visible de una web.
|
|
5
|
+
El objetivo es únicamente **identificar la entrada y abrir su editor**. No es
|
|
6
|
+
edición inline y no identifica campos individuales.
|
|
7
|
+
|
|
8
|
+
## Contrato obligatorio
|
|
9
|
+
|
|
10
|
+
Toda región visual cuyo contenido editorial provenga de un `ContentEntry` debe
|
|
11
|
+
marcar su elemento DOM exterior significativo con:
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<section
|
|
15
|
+
data-bacano-content-entry="v1"
|
|
16
|
+
data-bacano-entry-key="seccion-de-servicios"
|
|
17
|
+
data-bacano-entry-name="Sección de servicios"
|
|
18
|
+
data-bacano-entry-locale="es-CO"
|
|
19
|
+
>
|
|
20
|
+
<!-- contenido visible -->
|
|
21
|
+
</section>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Usar el helper oficial evita diferencias en nombres, versión y normalización:
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import {
|
|
28
|
+
getContentEntryLocatorAttributes,
|
|
29
|
+
type ContentEntry,
|
|
30
|
+
} from "@bacano/sdk";
|
|
31
|
+
|
|
32
|
+
function ServicesSection({ entry }: { entry: ContentEntry }) {
|
|
33
|
+
return (
|
|
34
|
+
<section {...getContentEntryLocatorAttributes(entry)}>
|
|
35
|
+
{/* La sección puede consumir un view model derivado de entry. */}
|
|
36
|
+
</section>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
El helper falla si la entrada no tiene una `key` estable o un `locale`. Una
|
|
42
|
+
región que no puede abrir una entrada inequívoca no debe aparentar que sí puede.
|
|
43
|
+
|
|
44
|
+
## Granularidad
|
|
45
|
+
|
|
46
|
+
- Marcar una vez el bloque visual administrable: `section`, `article`, banner,
|
|
47
|
+
tarjeta o contenedor equivalente.
|
|
48
|
+
- No marcar títulos, párrafos, botones ni campos individuales dentro del mismo
|
|
49
|
+
bloque.
|
|
50
|
+
- Si una tarjeta visible usa otra entrada referenciada, la tarjeta recibe su
|
|
51
|
+
propio marcador anidado. El espacio restante conserva el marcador de la
|
|
52
|
+
entrada contenedora.
|
|
53
|
+
- Si una entrada produce varias regiones independientes, cada región puede
|
|
54
|
+
repetir el mismo marcador.
|
|
55
|
+
- No marcar texto perteneciente al código/i18n, productos del catálogo,
|
|
56
|
+
configuración pública ni documentos legales como si fueran Content.
|
|
57
|
+
|
|
58
|
+
Los mappers pueden transformar `contentJson`, pero deben conservar el origen
|
|
59
|
+
editorial necesario para llamar el helper. Por ejemplo:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const section = {
|
|
63
|
+
locator: getContentEntryLocatorAttributes(entry),
|
|
64
|
+
title: readTitle(entry.contentJson),
|
|
65
|
+
};
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
<section {...section.locator}>{section.title}</section>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Responsabilidades del consumidor
|
|
73
|
+
|
|
74
|
+
Una extensión o herramienta de inspección:
|
|
75
|
+
|
|
76
|
+
1. busca `[data-bacano-content-entry="v1"]`;
|
|
77
|
+
2. muestra `data-bacano-entry-name` al usuario;
|
|
78
|
+
3. usa `data-bacano-entry-key` y `data-bacano-entry-locale` para construir la
|
|
79
|
+
navegación hacia el backoffice;
|
|
80
|
+
4. deja autenticación, empresa y permisos bajo control de Bacano.
|
|
81
|
+
|
|
82
|
+
El SDK no publica una URL de administración ni abre ventanas. Ese destino
|
|
83
|
+
pertenece a la herramienta interna y puede cambiar entre ambientes.
|
|
84
|
+
|
|
85
|
+
## Seguridad y salida estática
|
|
86
|
+
|
|
87
|
+
Los cuatro atributos contienen exclusivamente identidad editorial ya pública:
|
|
88
|
+
versión, key, nombre y locale de una entrada publicada. Nunca incluir tokens,
|
|
89
|
+
URLs administrativas firmadas, secretos, IDs de usuarios ni datos privados.
|
|
90
|
+
|
|
91
|
+
Los marcadores no conceden permisos. El backoffice debe autenticar y autorizar
|
|
92
|
+
cualquier lectura o modificación. En exports estáticos los atributos quedan
|
|
93
|
+
materializados junto con el contenido publicado y se actualizan en el siguiente
|
|
94
|
+
build.
|
|
95
|
+
|
|
96
|
+
## Verificación
|
|
97
|
+
|
|
98
|
+
Antes de publicar una web que usa Bacano Content:
|
|
99
|
+
|
|
100
|
+
- inventariar las entradas que producen regiones visibles;
|
|
101
|
+
- inspeccionar el HTML final, no solo JSX o componentes;
|
|
102
|
+
- comprobar que cada región tiene exactamente la key, nombre y locale de su
|
|
103
|
+
entrada fuente;
|
|
104
|
+
- probar marcadores anidados cuando una entrada contenedora renderiza entradas
|
|
105
|
+
referenciadas;
|
|
106
|
+
- comprobar que activar una herramienta de localización no altera navegación,
|
|
107
|
+
layout ni accesibilidad cuando vuelve a desactivarse.
|
|
108
|
+
|
|
109
|
+
Una región editorial sin marcador o con una key distinta de su fuente es un
|
|
110
|
+
fallo de integración, porque impide localizar de manera confiable el contenido
|
|
111
|
+
que la web declara administrable.
|
package/STOREFRONT_FORMS.md
CHANGED
|
@@ -4,6 +4,12 @@ Los formularios públicos de Bacano son definiciones tipadas y versionadas. El
|
|
|
4
4
|
storefront decide dónde se muestran y cómo lucen; Bacano controla su estructura,
|
|
5
5
|
estado y validaciones. La definición nunca contiene HTML ni JavaScript arbitrario.
|
|
6
6
|
|
|
7
|
+
Un agente puede crear y editar estas mismas definiciones desde
|
|
8
|
+
`@bacano/sdk/content-management`, con el acceso temporal de **Conectar agente**.
|
|
9
|
+
Lee la sección de formularios en `CONTENT_TYPE_MANAGEMENT.md`: `forms:manage`
|
|
10
|
+
prepara borradores y `forms:publish` permite publicar con aprobación. Nunca
|
|
11
|
+
incluyas ese cliente administrativo ni su token en el storefront público.
|
|
12
|
+
|
|
7
13
|
## Renderer oficial para React
|
|
8
14
|
|
|
9
15
|
Monta un único `BacanoProvider` global y usa la key pública configurada en el ERP:
|
|
@@ -81,6 +87,47 @@ El renderer personalizado puede cambiar componentes y estilos, pero debe usar
|
|
|
81
87
|
los campos, keys, opciones y validaciones entregados por el SDK. No debe crear
|
|
82
88
|
un segundo cliente, resolver el website nuevamente ni usar GraphQL directo.
|
|
83
89
|
|
|
90
|
+
## Honeypot y confirmación de persistencia
|
|
91
|
+
|
|
92
|
+
`honeypot` es un campo trampa contra bots, no un token de seguridad. No es una
|
|
93
|
+
marca de tiempo, un UUID, un número aleatorio ni un identificador del visitante.
|
|
94
|
+
Un usuario real debe enviarlo siempre vacío.
|
|
95
|
+
|
|
96
|
+
- `BacanoForm` administra el honeypot internamente. El storefront no debe crear
|
|
97
|
+
otro ni asignarle un valor.
|
|
98
|
+
- Un renderer personalizado debe omitir `honeypot` en
|
|
99
|
+
`client.forms.submitResponse(...)` salvo que implemente deliberadamente un
|
|
100
|
+
input trampa oculto. El SDK lo enviará como cadena vacía por defecto.
|
|
101
|
+
- Si se implementa el input trampa, debe quedar fuera del flujo visual y del
|
|
102
|
+
teclado, con autocompletado desactivado. Nunca debe reutilizar un campo real ni
|
|
103
|
+
recibir sugerencias del navegador o de un gestor de contraseñas.
|
|
104
|
+
- Bacano responde de manera deliberadamente genérica ante un honeypot lleno:
|
|
105
|
+
`accepted: true` y `responseId: null`. La respuesta no se persiste y el bot no
|
|
106
|
+
descubre que fue detectado.
|
|
107
|
+
- Para una prueba funcional o aprobación de producción, el envío debe devolver
|
|
108
|
+
`accepted: true` y un UUID en `responseId`, y debe aparecer una sola vez en
|
|
109
|
+
Respuestas dentro de Bacano.
|
|
110
|
+
|
|
111
|
+
No uses `responseId: null` para mostrar información distinta al visitante. Es
|
|
112
|
+
una señal técnica para pruebas y observabilidad segura, no un mensaje que deba
|
|
113
|
+
revelar la protección antispam.
|
|
114
|
+
|
|
115
|
+
## Tipos semánticos de contacto
|
|
116
|
+
|
|
117
|
+
La etiqueta y la key no sustituyen el tipo del campo. Si un dato representa un
|
|
118
|
+
correo o un teléfono, el formulario debe definirlo como `email` o `phone`:
|
|
119
|
+
|
|
120
|
+
- Bacano valida el formato según el tipo declarado.
|
|
121
|
+
- Las respuestas conservan todos los valores, pero solo los campos `email` y
|
|
122
|
+
`phone` alimentan los datos de contacto normalizados de la respuesta.
|
|
123
|
+
- Un campo `text` llamado `correo`, `email`, `telefono` o similar continúa
|
|
124
|
+
siendo texto libre. No debe dependerse de su nombre para CRM, leads o futuras
|
|
125
|
+
automatizaciones.
|
|
126
|
+
|
|
127
|
+
Antes de publicar, revisa que los campos destinados a identificar o contactar a
|
|
128
|
+
la persona tengan el tipo correcto. Cambiar únicamente su etiqueta no corrige el
|
|
129
|
+
contrato de datos.
|
|
130
|
+
|
|
84
131
|
## Next.js y Static First
|
|
85
132
|
|
|
86
133
|
Obtén la definición durante el build o desde un Server Component usando el
|
|
@@ -127,15 +174,32 @@ consultarlo.
|
|
|
127
174
|
validación del navegador mejora la experiencia, pero no es autoritativa.
|
|
128
175
|
- URL, referidor y atribución se sanean. Las respuestas y datos personales no
|
|
129
176
|
se escriben en logs.
|
|
130
|
-
-
|
|
131
|
-
|
|
132
|
-
|
|
177
|
+
- `pageUrl`, `referrerUrl` y la URL de atribución aceptan únicamente HTTP o
|
|
178
|
+
HTTPS. No envíes protocolos locales, ejecutables o de datos.
|
|
179
|
+
- Los teléfonos admiten un `+` inicial y separadores habituales; Bacano los
|
|
180
|
+
normaliza antes de resolver identidad y rechaza letras o signos mal ubicados.
|
|
181
|
+
- Los números usan notación decimal o científica explícita. Valores ambiguos
|
|
182
|
+
como `0x10` o `3 unidades` se rechazan aunque JavaScript pudiera convertirlos.
|
|
183
|
+
- Un checkbox opcional omitido se conserva como `null`; uno requerido debe
|
|
184
|
+
enviarse expresamente como `true`.
|
|
185
|
+
- Una falla de correo, CRM o automatización no revierte la respuesta.
|
|
186
|
+
- `STORE_RESPONSE` conserva la respuesta sin crear un lead.
|
|
187
|
+
- `CAPTURE_LEAD` conserva primero la respuesta y después resuelve internamente
|
|
188
|
+
la identidad canónica. Reutiliza un lead o cliente existente, crea un lead
|
|
189
|
+
cuando corresponde y manda a revisión las coincidencias contradictorias de
|
|
190
|
+
correo y teléfono sin fusionarlas automáticamente.
|
|
191
|
+
- El resultado público continúa siendo deliberadamente genérico. El storefront
|
|
192
|
+
no recibe ni debe inferir si Bacano creó un lead, encontró un cliente o marcó
|
|
193
|
+
una identidad para revisión.
|
|
133
194
|
|
|
134
195
|
## Checklist mínimo del storefront
|
|
135
196
|
|
|
136
197
|
- Usar un solo `BacanoProvider` y el cliente global ya inicializado.
|
|
137
198
|
- Renderizar con `BacanoForm` o con `useBacanoForm`; nunca copiar la definición
|
|
138
199
|
a un esquema local ni consultar GraphQL directamente.
|
|
200
|
+
- Pueden coexistir varios `BacanoForm` en una página. El renderer oficial aísla
|
|
201
|
+
los identificadores de sus controles; no construyas IDs globales solo con la
|
|
202
|
+
key del campo.
|
|
139
203
|
- Entregar `initialForm` cuando la página se genera de forma estática.
|
|
140
204
|
- Respetar `ACTIVE`, `PAUSED`, versión y `definitionHash` sin inferir estados.
|
|
141
205
|
- Mantener una `idempotencyKey` estable durante los reintentos del mismo envío.
|
|
@@ -143,5 +207,11 @@ consultarlo.
|
|
|
143
207
|
reintento idempotente y error de red.
|
|
144
208
|
- Confirmar en Bacano que una respuesta real quedó persistida antes de aprobar
|
|
145
209
|
el flujo; no basta con mostrar el mensaje de éxito del navegador.
|
|
146
|
-
-
|
|
147
|
-
|
|
210
|
+
- Probar autocompletado del navegador y gestores de contraseñas: ningún campo
|
|
211
|
+
real puede llenar el honeypot. Un envío real debe devolver un UUID como
|
|
212
|
+
`responseId`; `null` no demuestra persistencia.
|
|
213
|
+
- Verificar que correos y teléfonos utilicen los tipos `email` y `phone`, no
|
|
214
|
+
campos de texto con una etiqueta parecida.
|
|
215
|
+
- No prometer correo, cupón, solicitud ni automatización mientras la capacidad
|
|
216
|
+
no esté implementada y activa en Bacano. Aunque `CAPTURE_LEAD` esté activo,
|
|
217
|
+
el mensaje público no debe afirmar qué identidad interna se creó o reutilizó.
|
package/STOREFRONT_LEADS.md
CHANGED
|
@@ -50,7 +50,7 @@ if (!consent) return null;
|
|
|
50
50
|
// Conserva esta clave en el estado del formulario mientras reintentas el envío.
|
|
51
51
|
const idempotencyKey = crypto.randomUUID();
|
|
52
52
|
const result = await client.marketing.captureLead({
|
|
53
|
-
campaignKey: "newsletter-
|
|
53
|
+
campaignKey: "newsletter-primavera",
|
|
54
54
|
idempotencyKey,
|
|
55
55
|
commercialConsent: true,
|
|
56
56
|
contact: {
|
package/STOREFRONT_PREVIEW.md
CHANGED
|
@@ -227,6 +227,7 @@ export default function PreviewPage() {
|
|
|
227
227
|
"use client";
|
|
228
228
|
|
|
229
229
|
import { useSearchParams } from "next/navigation";
|
|
230
|
+
import { useState } from "react";
|
|
230
231
|
import {
|
|
231
232
|
BacanoPreviewShell,
|
|
232
233
|
interceptPreviewLink,
|
|
@@ -265,7 +266,11 @@ const loadRoute: PreviewRouteLoader<PreviewDocument> = async ({
|
|
|
265
266
|
|
|
266
267
|
export function PreviewClient() {
|
|
267
268
|
const searchParams = useSearchParams();
|
|
268
|
-
|
|
269
|
+
// Initial value only. Held in state afterwards so `navigate` can change it
|
|
270
|
+
// without a reload; listen for `popstate` to keep the back button working.
|
|
271
|
+
const [route, setRoute] = useState(() =>
|
|
272
|
+
normalizePreviewRoute(searchParams.get("route")),
|
|
273
|
+
);
|
|
269
274
|
|
|
270
275
|
return (
|
|
271
276
|
<BacanoPreviewShell
|
|
@@ -274,9 +279,17 @@ export function PreviewClient() {
|
|
|
274
279
|
loadRoute={loadRoute}
|
|
275
280
|
isPreviewableRoute={(nextRoute) => previewable.has(nextRoute)}
|
|
276
281
|
navigate={(nextRoute) => {
|
|
277
|
-
|
|
282
|
+
// pushState, not location.assign. A full navigation unmounts the
|
|
283
|
+
// provider, and its route cache lives in a ref on it — so assigning
|
|
284
|
+
// location throws away every already-loaded route and refetches the
|
|
285
|
+
// one you are navigating to. Keep the route in state and let the
|
|
286
|
+
// provider reload it in place.
|
|
287
|
+
window.history.pushState(
|
|
288
|
+
null,
|
|
289
|
+
"",
|
|
278
290
|
`/vista-previa/?route=${encodeURIComponent(nextRoute)}`,
|
|
279
291
|
);
|
|
292
|
+
setRoute(nextRoute);
|
|
280
293
|
}}
|
|
281
294
|
toolbar={{
|
|
282
295
|
websiteName: "My store",
|
|
@@ -414,10 +427,10 @@ When the storefront follows this contract:
|
|
|
414
427
|
The build still depends on whatever Bacano data the public static pages request
|
|
415
428
|
at build time. Preview does not change or replace that build contract.
|
|
416
429
|
|
|
417
|
-
## Validated reference implementation
|
|
430
|
+
## Validated reference implementation
|
|
418
431
|
|
|
419
|
-
|
|
420
|
-
2026-08-06. It uses one global `BacanoProvider`; `BacanoPreviewShell` consumes
|
|
432
|
+
A customer storefront validated the official integration with `@bacano/sdk`
|
|
433
|
+
`1.0.5` on 2026-08-06. It uses one global `BacanoProvider`; `BacanoPreviewShell` consumes
|
|
421
434
|
that initialized client and the loader receives `{ route, force, client }`.
|
|
422
435
|
|
|
423
436
|
The validated static production build generated all `417/417` pages. TypeScript,
|
|
@@ -435,7 +448,7 @@ The implementation removed these generic storefront responsibilities:
|
|
|
435
448
|
- Pathname-based marketing suppression inside preview renderers once the SDK
|
|
436
449
|
boundary covers them.
|
|
437
450
|
|
|
438
|
-
|
|
451
|
+
That storefront retained these storefront-specific responsibilities:
|
|
439
452
|
|
|
440
453
|
- `/vista-previa/page.tsx` and its `noindex` metadata.
|
|
441
454
|
- Sitemap exclusion.
|
|
@@ -445,9 +458,9 @@ Baby Rex retained these storefront-specific responsibilities:
|
|
|
445
458
|
content-driven catalog sections.
|
|
446
459
|
- `PreviewDocumentView` and all production renderers.
|
|
447
460
|
- Storefront-specific skeleton, missing-content, and unsupported-route views.
|
|
448
|
-
- The Next.js adapter that writes `?
|
|
461
|
+
- The Next.js adapter that writes `?route={encodedRoute}`.
|
|
449
462
|
|
|
450
|
-
|
|
463
|
+
It is the implementation reference for integration shape, not a source to
|
|
451
464
|
copy blindly. Other storefronts keep their own route maps, Content keys,
|
|
452
465
|
mappers, and visual components.
|
|
453
466
|
|
|
@@ -124,7 +124,7 @@ confirmado.
|
|
|
124
124
|
|
|
125
125
|
## Prohibiciones
|
|
126
126
|
|
|
127
|
-
- No calcular umbrales en
|
|
127
|
+
- No calcular umbrales en el storefront.
|
|
128
128
|
- No seleccionar `WHOLESALE` directamente desde el navegador.
|
|
129
129
|
- No enviar precios en operaciones de carrito o checkout.
|
|
130
130
|
- No consultar Hasura ni tablas de precios mediante GraphQL directo.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ContentEntry } from "./types";
|
|
2
|
+
export declare const CONTENT_ENTRY_LOCATOR_VERSION: "v1";
|
|
3
|
+
export declare const CONTENT_ENTRY_LOCATOR_SELECTOR: "[data-bacano-content-entry=\"v1\"]";
|
|
4
|
+
export type ContentEntryLocatorSource = Pick<ContentEntry, "key" | "locale" | "name">;
|
|
5
|
+
export interface ContentEntryLocatorAttributes {
|
|
6
|
+
"data-bacano-content-entry": typeof CONTENT_ENTRY_LOCATOR_VERSION;
|
|
7
|
+
"data-bacano-entry-key": string;
|
|
8
|
+
"data-bacano-entry-locale": string;
|
|
9
|
+
"data-bacano-entry-name": string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Adds the public, non-authoritative marker consumed by Bacano's content
|
|
13
|
+
* locator. Spread the result only on the outermost meaningful DOM element
|
|
14
|
+
* whose visible editorial content is owned by the entry.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getContentEntryLocatorAttributes(entry: ContentEntryLocatorSource): ContentEntryLocatorAttributes;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { FormManagementClient } from "./form-management-types";
|
|
1
2
|
export type ContentManagementTypeKind = "entry" | "component";
|
|
2
3
|
export type ContentManagementTypeStatus = "draft" | "active" | "archived";
|
|
3
4
|
export type ContentManagementTypeUsage = "website_content" | "product_additional" | "product_specification";
|
|
4
|
-
export type ContentManagementScope = "content_types:manage" | "content_entries:manage" | "content_media:upload" | "content_entries:publish";
|
|
5
|
+
export type ContentManagementScope = "content_types:manage" | "content_entries:manage" | "content_media:upload" | "content_entries:publish" | "forms:manage" | "forms:publish";
|
|
5
6
|
export type ContentManagementFieldType = "boolean" | "component" | "date" | "link" | "measure" | "media" | "number" | "reference" | "repeater" | "rich_text" | "rich_text_full" | "select" | "slug" | "text" | "textarea";
|
|
6
7
|
export interface ContentManagementChildFieldInput {
|
|
7
8
|
id?: string | null;
|
|
@@ -65,7 +66,7 @@ export interface ContentManagementType extends Omit<ContentManagementTypeInput,
|
|
|
65
66
|
export interface ContentManagementContext {
|
|
66
67
|
companyId: string;
|
|
67
68
|
/** @deprecated Use scopes to inspect the complete grant. */
|
|
68
|
-
scope:
|
|
69
|
+
scope: ContentManagementScope;
|
|
69
70
|
scopes: ContentManagementScope[];
|
|
70
71
|
expiresAt: string;
|
|
71
72
|
}
|
|
@@ -259,7 +260,7 @@ export interface ContentManagementClientConfig {
|
|
|
259
260
|
accessToken: string | ContentManagementAccessTokenProvider;
|
|
260
261
|
fetch?: typeof fetch;
|
|
261
262
|
}
|
|
262
|
-
export interface ContentManagementClient {
|
|
263
|
+
export interface ContentManagementClient extends FormManagementClient {
|
|
263
264
|
getContext(): Promise<ContentManagementContext>;
|
|
264
265
|
getReferences(): Promise<ContentManagementReferences>;
|
|
265
266
|
listTypes(): Promise<ContentManagementType[]>;
|
package/content-management.cjs
CHANGED
|
@@ -24,6 +24,42 @@ __export(content_management_exports, {
|
|
|
24
24
|
createContentManagementClient: () => createContentManagementClient
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(content_management_exports);
|
|
27
|
+
|
|
28
|
+
// libs/sdk/src/form-management.ts
|
|
29
|
+
function createFormManagementMethods(request) {
|
|
30
|
+
const path = (id) => `/forms/${encodeURIComponent(id.trim())}`;
|
|
31
|
+
return {
|
|
32
|
+
getFormReferences: () => request("/forms/references"),
|
|
33
|
+
listForms: (input = {}) => {
|
|
34
|
+
const search = new URLSearchParams();
|
|
35
|
+
for (const key of ["websiteId", "key", "limit", "offset"]) {
|
|
36
|
+
if (input[key] !== void 0) search.set(key, String(input[key]));
|
|
37
|
+
}
|
|
38
|
+
return request(
|
|
39
|
+
`/forms${search.size ? `?${search}` : ""}`
|
|
40
|
+
);
|
|
41
|
+
},
|
|
42
|
+
getForm: (id) => request(path(id)),
|
|
43
|
+
createForm: (input) => request("/forms", {
|
|
44
|
+
method: "POST",
|
|
45
|
+
body: JSON.stringify(input)
|
|
46
|
+
}),
|
|
47
|
+
updateForm: (id, input) => request(path(id), {
|
|
48
|
+
method: "PUT",
|
|
49
|
+
body: JSON.stringify(input)
|
|
50
|
+
}),
|
|
51
|
+
publishForm: (id, input) => request(`${path(id)}/publish`, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
body: JSON.stringify(input)
|
|
54
|
+
}),
|
|
55
|
+
changeFormStatus: (id, input) => request(`${path(id)}/status`, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
body: JSON.stringify(input)
|
|
58
|
+
})
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// libs/sdk/src/content-management.ts
|
|
27
63
|
var ContentManagementError = class extends Error {
|
|
28
64
|
code;
|
|
29
65
|
status;
|
|
@@ -145,6 +181,7 @@ function createContentManagementClient(config) {
|
|
|
145
181
|
}
|
|
146
182
|
);
|
|
147
183
|
return {
|
|
184
|
+
...createFormManagementMethods(request),
|
|
148
185
|
getContext: () => request("/context"),
|
|
149
186
|
getReferences: () => request("/references"),
|
|
150
187
|
listTypes: () => request("/types"),
|
package/content-management.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ContentManagementClient, ContentManagementClientConfig } from "./content-management-types";
|
|
2
2
|
export * from "./content-management-types";
|
|
3
|
+
export * from "./form-management-types";
|
|
3
4
|
export declare class ContentManagementError extends Error {
|
|
4
5
|
readonly code: string;
|
|
5
6
|
readonly status: number;
|
package/content-management.js
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
// libs/sdk/src/form-management.ts
|
|
2
|
+
function createFormManagementMethods(request) {
|
|
3
|
+
const path = (id) => `/forms/${encodeURIComponent(id.trim())}`;
|
|
4
|
+
return {
|
|
5
|
+
getFormReferences: () => request("/forms/references"),
|
|
6
|
+
listForms: (input = {}) => {
|
|
7
|
+
const search = new URLSearchParams();
|
|
8
|
+
for (const key of ["websiteId", "key", "limit", "offset"]) {
|
|
9
|
+
if (input[key] !== void 0) search.set(key, String(input[key]));
|
|
10
|
+
}
|
|
11
|
+
return request(
|
|
12
|
+
`/forms${search.size ? `?${search}` : ""}`
|
|
13
|
+
);
|
|
14
|
+
},
|
|
15
|
+
getForm: (id) => request(path(id)),
|
|
16
|
+
createForm: (input) => request("/forms", {
|
|
17
|
+
method: "POST",
|
|
18
|
+
body: JSON.stringify(input)
|
|
19
|
+
}),
|
|
20
|
+
updateForm: (id, input) => request(path(id), {
|
|
21
|
+
method: "PUT",
|
|
22
|
+
body: JSON.stringify(input)
|
|
23
|
+
}),
|
|
24
|
+
publishForm: (id, input) => request(`${path(id)}/publish`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
body: JSON.stringify(input)
|
|
27
|
+
}),
|
|
28
|
+
changeFormStatus: (id, input) => request(`${path(id)}/status`, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
body: JSON.stringify(input)
|
|
31
|
+
})
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
1
35
|
// libs/sdk/src/content-management.ts
|
|
2
36
|
var ContentManagementError = class extends Error {
|
|
3
37
|
code;
|
|
@@ -120,6 +154,7 @@ function createContentManagementClient(config) {
|
|
|
120
154
|
}
|
|
121
155
|
);
|
|
122
156
|
return {
|
|
157
|
+
...createFormManagementMethods(request),
|
|
123
158
|
getContext: () => request("/context"),
|
|
124
159
|
getReferences: () => request("/references"),
|
|
125
160
|
listTypes: () => request("/types"),
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { WebsiteFormAction, WebsiteFormDefinition } from "./types";
|
|
2
|
+
export interface ManagedFormInput {
|
|
3
|
+
websiteId: string;
|
|
4
|
+
key: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
action?: WebsiteFormAction;
|
|
8
|
+
notifyByEmail?: boolean;
|
|
9
|
+
notificationRecipients?: string[];
|
|
10
|
+
definition: WebsiteFormDefinition;
|
|
11
|
+
}
|
|
12
|
+
export interface ManagedForm extends Required<ManagedFormInput> {
|
|
13
|
+
id: string;
|
|
14
|
+
companyId: string;
|
|
15
|
+
status: "DRAFT" | "ACTIVE" | "PAUSED" | "ARCHIVED";
|
|
16
|
+
publishedVersion: number | null;
|
|
17
|
+
createdAt: string;
|
|
18
|
+
updatedAt: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ManagedFormListInput {
|
|
21
|
+
websiteId?: string;
|
|
22
|
+
key?: string;
|
|
23
|
+
limit?: number;
|
|
24
|
+
offset?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface ManagedFormList {
|
|
27
|
+
forms: ManagedForm[];
|
|
28
|
+
total: number;
|
|
29
|
+
}
|
|
30
|
+
export interface ManagedFormReferences {
|
|
31
|
+
websites: Array<{
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
slug: string;
|
|
35
|
+
isActive: boolean;
|
|
36
|
+
}>;
|
|
37
|
+
optionLists: Array<{
|
|
38
|
+
key: string;
|
|
39
|
+
name: string;
|
|
40
|
+
options: Array<{
|
|
41
|
+
value: string;
|
|
42
|
+
label: string;
|
|
43
|
+
}>;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
export interface FormManagementClient {
|
|
47
|
+
getFormReferences(): Promise<ManagedFormReferences>;
|
|
48
|
+
listForms(input?: ManagedFormListInput): Promise<ManagedFormList>;
|
|
49
|
+
getForm(formId: string): Promise<ManagedForm>;
|
|
50
|
+
createForm(input: ManagedFormInput): Promise<ManagedForm>;
|
|
51
|
+
/** Replaces the full draft. Preserve existing fields and use the last read revision. */
|
|
52
|
+
updateForm(formId: string, input: ManagedFormInput & {
|
|
53
|
+
expectedUpdatedAt: string;
|
|
54
|
+
}): Promise<ManagedForm>;
|
|
55
|
+
publishForm(formId: string, input: {
|
|
56
|
+
expectedUpdatedAt: string;
|
|
57
|
+
}): Promise<ManagedForm>;
|
|
58
|
+
changeFormStatus(formId: string, input: {
|
|
59
|
+
status: "ACTIVE" | "PAUSED" | "ARCHIVED";
|
|
60
|
+
expectedUpdatedAt: string;
|
|
61
|
+
}): Promise<ManagedForm>;
|
|
62
|
+
}
|
package/index.cjs
CHANGED
|
@@ -21,6 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
AuthError: () => AuthError,
|
|
24
|
+
CONTENT_ENTRY_LOCATOR_SELECTOR: () => CONTENT_ENTRY_LOCATOR_SELECTOR,
|
|
25
|
+
CONTENT_ENTRY_LOCATOR_VERSION: () => CONTENT_ENTRY_LOCATOR_VERSION,
|
|
24
26
|
CheckoutError: () => CheckoutError,
|
|
25
27
|
GraphQLRequestError: () => GraphQLRequestError,
|
|
26
28
|
MARKETING_SCHEMA_VERSION: () => MARKETING_SCHEMA_VERSION,
|
|
@@ -38,6 +40,7 @@ __export(index_exports, {
|
|
|
38
40
|
createWebsiteMarketingRuntime: () => createWebsiteMarketingRuntime,
|
|
39
41
|
formatMediaSrcSet: () => formatMediaSrcSet,
|
|
40
42
|
getAllowedMarketingParameters: () => getAllowedMarketingParameters,
|
|
43
|
+
getContentEntryLocatorAttributes: () => getContentEntryLocatorAttributes,
|
|
41
44
|
getPrimaryProductImage: () => getPrimaryProductImage,
|
|
42
45
|
getProductDetailVariants: () => getProductDetailVariants,
|
|
43
46
|
getProductImages: () => getProductImages,
|
|
@@ -6904,3 +6907,23 @@ function createBacanoClient(config) {
|
|
|
6904
6907
|
}
|
|
6905
6908
|
};
|
|
6906
6909
|
}
|
|
6910
|
+
|
|
6911
|
+
// libs/sdk/src/content-entry-locator.ts
|
|
6912
|
+
var CONTENT_ENTRY_LOCATOR_VERSION = "v1";
|
|
6913
|
+
var CONTENT_ENTRY_LOCATOR_SELECTOR = `[data-bacano-content-entry="${CONTENT_ENTRY_LOCATOR_VERSION}"]`;
|
|
6914
|
+
function getContentEntryLocatorAttributes(entry) {
|
|
6915
|
+
const key = entry.key?.trim();
|
|
6916
|
+
if (!key) {
|
|
6917
|
+
throw new Error("A Bacano Content locator requires a stable entry key.");
|
|
6918
|
+
}
|
|
6919
|
+
const locale = entry.locale.trim();
|
|
6920
|
+
if (!locale) {
|
|
6921
|
+
throw new Error("A Bacano Content locator requires an entry locale.");
|
|
6922
|
+
}
|
|
6923
|
+
return {
|
|
6924
|
+
"data-bacano-content-entry": CONTENT_ENTRY_LOCATOR_VERSION,
|
|
6925
|
+
"data-bacano-entry-key": key,
|
|
6926
|
+
"data-bacano-entry-locale": locale,
|
|
6927
|
+
"data-bacano-entry-name": entry.name.trim() || key
|
|
6928
|
+
};
|
|
6929
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type { BacanoClient } from "./client";
|
|
2
2
|
export { createBacanoClient } from "./client";
|
|
3
|
+
export type { ContentEntryLocatorAttributes, ContentEntryLocatorSource, } from "./content-entry-locator";
|
|
4
|
+
export { CONTENT_ENTRY_LOCATOR_SELECTOR, CONTENT_ENTRY_LOCATOR_VERSION, getContentEntryLocatorAttributes, } from "./content-entry-locator";
|
|
3
5
|
export { AuthError, CheckoutError, GraphQLRequestError, NotInitializedError, RateLimitError, WebsiteError, } from "./errors";
|
|
4
6
|
export type { WebsiteAdvertisingConsent, WebsiteMarketingAttribution, WebsiteMarketingBrowserEvent, WebsiteMarketingBrowserEventName, WebsiteMarketingContactPlacement, WebsiteMarketingDiagnostic, WebsiteMarketingItem, WebsiteMarketingRuntime, WebsiteMarketingRuntimeOptions, WebsiteMarketingScriptLoadStrategy, WebsiteMarketingTouchpoint, } from "./marketing";
|
|
5
7
|
export { captureWebsiteMarketingAttribution, createWebsiteMarketingRuntime, getAllowedMarketingParameters, getWebsiteMarketingAttribution, MARKETING_SCHEMA_VERSION, sanitizeMarketingUrl, WEBSITE_MARKETING_CONTACT_PLACEMENTS, } from "./marketing";
|
package/index.js
CHANGED
|
@@ -6850,8 +6850,30 @@ function createBacanoClient(config) {
|
|
|
6850
6850
|
}
|
|
6851
6851
|
};
|
|
6852
6852
|
}
|
|
6853
|
+
|
|
6854
|
+
// libs/sdk/src/content-entry-locator.ts
|
|
6855
|
+
var CONTENT_ENTRY_LOCATOR_VERSION = "v1";
|
|
6856
|
+
var CONTENT_ENTRY_LOCATOR_SELECTOR = `[data-bacano-content-entry="${CONTENT_ENTRY_LOCATOR_VERSION}"]`;
|
|
6857
|
+
function getContentEntryLocatorAttributes(entry) {
|
|
6858
|
+
const key = entry.key?.trim();
|
|
6859
|
+
if (!key) {
|
|
6860
|
+
throw new Error("A Bacano Content locator requires a stable entry key.");
|
|
6861
|
+
}
|
|
6862
|
+
const locale = entry.locale.trim();
|
|
6863
|
+
if (!locale) {
|
|
6864
|
+
throw new Error("A Bacano Content locator requires an entry locale.");
|
|
6865
|
+
}
|
|
6866
|
+
return {
|
|
6867
|
+
"data-bacano-content-entry": CONTENT_ENTRY_LOCATOR_VERSION,
|
|
6868
|
+
"data-bacano-entry-key": key,
|
|
6869
|
+
"data-bacano-entry-locale": locale,
|
|
6870
|
+
"data-bacano-entry-name": entry.name.trim() || key
|
|
6871
|
+
};
|
|
6872
|
+
}
|
|
6853
6873
|
export {
|
|
6854
6874
|
AuthError,
|
|
6875
|
+
CONTENT_ENTRY_LOCATOR_SELECTOR,
|
|
6876
|
+
CONTENT_ENTRY_LOCATOR_VERSION,
|
|
6855
6877
|
CheckoutError,
|
|
6856
6878
|
GraphQLRequestError,
|
|
6857
6879
|
MARKETING_SCHEMA_VERSION,
|
|
@@ -6869,6 +6891,7 @@ export {
|
|
|
6869
6891
|
createWebsiteMarketingRuntime,
|
|
6870
6892
|
formatMediaSrcSet,
|
|
6871
6893
|
getAllowedMarketingParameters,
|
|
6894
|
+
getContentEntryLocatorAttributes,
|
|
6872
6895
|
getPrimaryProductImage,
|
|
6873
6896
|
getProductDetailVariants,
|
|
6874
6897
|
getProductImages,
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import type { BacanoFormValues } from "./forms";
|
|
|
3
3
|
type BacanoFormValue = BacanoFormValues[string];
|
|
4
4
|
export declare function BacanoFormFieldControl(props: {
|
|
5
5
|
field: WebsiteFormField;
|
|
6
|
+
id?: string;
|
|
6
7
|
className?: string;
|
|
7
8
|
value: BacanoFormValue;
|
|
8
9
|
onChange(value: BacanoFormValue): void;
|
package/react/forms.cjs
CHANGED
|
@@ -30,8 +30,9 @@ var import_react3 = require("react");
|
|
|
30
30
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
31
31
|
function BacanoFormFieldControl(props) {
|
|
32
32
|
const { field, className, value, onChange } = props;
|
|
33
|
+
const controlId = props.id ?? field.key;
|
|
33
34
|
const common = {
|
|
34
|
-
id:
|
|
35
|
+
id: controlId,
|
|
35
36
|
name: field.key,
|
|
36
37
|
required: field.required,
|
|
37
38
|
className
|
|
@@ -71,7 +72,7 @@ function BacanoFormFieldControl(props) {
|
|
|
71
72
|
);
|
|
72
73
|
}
|
|
73
74
|
if (field.type === "radio") {
|
|
74
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "radiogroup", "aria-labelledby": `${
|
|
75
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "radiogroup", "aria-labelledby": `${controlId}-label`, children: (field.options ?? []).map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
75
76
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
76
77
|
"input",
|
|
77
78
|
{
|
|
@@ -210,13 +211,14 @@ function useBacanoForm(formKey, options) {
|
|
|
210
211
|
}
|
|
211
212
|
function FormFields(props) {
|
|
212
213
|
return props.fields.map((field) => {
|
|
214
|
+
const controlId = `${props.idPrefix}-${field.key}`;
|
|
213
215
|
const value = props.values[field.key] ?? (field.type === "checkbox" ? false : field.type === "multi_select" ? [] : "");
|
|
214
216
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: props.classNames?.field, children: [
|
|
215
217
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
216
218
|
"label",
|
|
217
219
|
{
|
|
218
|
-
id: `${
|
|
219
|
-
htmlFor: field.type === "radio" ? void 0 :
|
|
220
|
+
id: `${controlId}-label`,
|
|
221
|
+
htmlFor: field.type === "radio" ? void 0 : controlId,
|
|
220
222
|
className: props.classNames?.label,
|
|
221
223
|
children: field.label
|
|
222
224
|
}
|
|
@@ -225,6 +227,7 @@ function FormFields(props) {
|
|
|
225
227
|
BacanoFormFieldControl,
|
|
226
228
|
{
|
|
227
229
|
field,
|
|
230
|
+
id: controlId,
|
|
228
231
|
value,
|
|
229
232
|
className: props.classNames?.input,
|
|
230
233
|
onChange: (nextValue) => props.onChange(field.key, nextValue)
|
|
@@ -242,6 +245,7 @@ function FormSections(props) {
|
|
|
242
245
|
FormFields,
|
|
243
246
|
{
|
|
244
247
|
fields: section.fields,
|
|
248
|
+
idPrefix: props.idPrefix,
|
|
245
249
|
values: props.values,
|
|
246
250
|
classNames: props.classNames,
|
|
247
251
|
onChange: props.onChange
|
|
@@ -250,6 +254,8 @@ function FormSections(props) {
|
|
|
250
254
|
] }, section.key));
|
|
251
255
|
}
|
|
252
256
|
function BacanoForm(props) {
|
|
257
|
+
const reactId = (0, import_react3.useId)();
|
|
258
|
+
const idPrefix = `${props.formKey}-${reactId.replace(/:/g, "")}`;
|
|
253
259
|
const { client } = useBacanoState();
|
|
254
260
|
const { form, loading, error } = useBacanoForm(props.formKey, {
|
|
255
261
|
initialForm: props.initialForm
|
|
@@ -382,6 +388,7 @@ function BacanoForm(props) {
|
|
|
382
388
|
FormFields,
|
|
383
389
|
{
|
|
384
390
|
fields: activeStep.fields,
|
|
391
|
+
idPrefix,
|
|
385
392
|
values,
|
|
386
393
|
classNames: props.classNames,
|
|
387
394
|
onChange: setFieldValue
|
|
@@ -391,6 +398,7 @@ function BacanoForm(props) {
|
|
|
391
398
|
FormSections,
|
|
392
399
|
{
|
|
393
400
|
sections: activeStep.sections,
|
|
401
|
+
idPrefix,
|
|
394
402
|
values,
|
|
395
403
|
classNames: props.classNames,
|
|
396
404
|
onChange: setFieldValue
|
|
@@ -401,6 +409,7 @@ function BacanoForm(props) {
|
|
|
401
409
|
FormFields,
|
|
402
410
|
{
|
|
403
411
|
fields,
|
|
412
|
+
idPrefix,
|
|
404
413
|
values,
|
|
405
414
|
classNames: props.classNames,
|
|
406
415
|
onChange: setFieldValue
|
|
@@ -410,6 +419,7 @@ function BacanoForm(props) {
|
|
|
410
419
|
FormSections,
|
|
411
420
|
{
|
|
412
421
|
sections,
|
|
422
|
+
idPrefix,
|
|
413
423
|
values,
|
|
414
424
|
classNames: props.classNames,
|
|
415
425
|
onChange: setFieldValue
|
package/react/forms.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
useCallback as useCallback2,
|
|
4
4
|
useEffect as useEffect2,
|
|
5
|
+
useId,
|
|
5
6
|
useMemo,
|
|
6
7
|
useRef as useRef2,
|
|
7
8
|
useState as useState2
|
|
@@ -11,8 +12,9 @@ import {
|
|
|
11
12
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
13
|
function BacanoFormFieldControl(props) {
|
|
13
14
|
const { field, className, value, onChange } = props;
|
|
15
|
+
const controlId = props.id ?? field.key;
|
|
14
16
|
const common = {
|
|
15
|
-
id:
|
|
17
|
+
id: controlId,
|
|
16
18
|
name: field.key,
|
|
17
19
|
required: field.required,
|
|
18
20
|
className
|
|
@@ -52,7 +54,7 @@ function BacanoFormFieldControl(props) {
|
|
|
52
54
|
);
|
|
53
55
|
}
|
|
54
56
|
if (field.type === "radio") {
|
|
55
|
-
return /* @__PURE__ */ jsx("div", { role: "radiogroup", "aria-labelledby": `${
|
|
57
|
+
return /* @__PURE__ */ jsx("div", { role: "radiogroup", "aria-labelledby": `${controlId}-label`, children: (field.options ?? []).map((option) => /* @__PURE__ */ jsxs("label", { children: [
|
|
56
58
|
/* @__PURE__ */ jsx(
|
|
57
59
|
"input",
|
|
58
60
|
{
|
|
@@ -191,13 +193,14 @@ function useBacanoForm(formKey, options) {
|
|
|
191
193
|
}
|
|
192
194
|
function FormFields(props) {
|
|
193
195
|
return props.fields.map((field) => {
|
|
196
|
+
const controlId = `${props.idPrefix}-${field.key}`;
|
|
194
197
|
const value = props.values[field.key] ?? (field.type === "checkbox" ? false : field.type === "multi_select" ? [] : "");
|
|
195
198
|
return /* @__PURE__ */ jsxs2("div", { className: props.classNames?.field, children: [
|
|
196
199
|
/* @__PURE__ */ jsx2(
|
|
197
200
|
"label",
|
|
198
201
|
{
|
|
199
|
-
id: `${
|
|
200
|
-
htmlFor: field.type === "radio" ? void 0 :
|
|
202
|
+
id: `${controlId}-label`,
|
|
203
|
+
htmlFor: field.type === "radio" ? void 0 : controlId,
|
|
201
204
|
className: props.classNames?.label,
|
|
202
205
|
children: field.label
|
|
203
206
|
}
|
|
@@ -206,6 +209,7 @@ function FormFields(props) {
|
|
|
206
209
|
BacanoFormFieldControl,
|
|
207
210
|
{
|
|
208
211
|
field,
|
|
212
|
+
id: controlId,
|
|
209
213
|
value,
|
|
210
214
|
className: props.classNames?.input,
|
|
211
215
|
onChange: (nextValue) => props.onChange(field.key, nextValue)
|
|
@@ -223,6 +227,7 @@ function FormSections(props) {
|
|
|
223
227
|
FormFields,
|
|
224
228
|
{
|
|
225
229
|
fields: section.fields,
|
|
230
|
+
idPrefix: props.idPrefix,
|
|
226
231
|
values: props.values,
|
|
227
232
|
classNames: props.classNames,
|
|
228
233
|
onChange: props.onChange
|
|
@@ -231,6 +236,8 @@ function FormSections(props) {
|
|
|
231
236
|
] }, section.key));
|
|
232
237
|
}
|
|
233
238
|
function BacanoForm(props) {
|
|
239
|
+
const reactId = useId();
|
|
240
|
+
const idPrefix = `${props.formKey}-${reactId.replace(/:/g, "")}`;
|
|
234
241
|
const { client } = useBacanoState();
|
|
235
242
|
const { form, loading, error } = useBacanoForm(props.formKey, {
|
|
236
243
|
initialForm: props.initialForm
|
|
@@ -363,6 +370,7 @@ function BacanoForm(props) {
|
|
|
363
370
|
FormFields,
|
|
364
371
|
{
|
|
365
372
|
fields: activeStep.fields,
|
|
373
|
+
idPrefix,
|
|
366
374
|
values,
|
|
367
375
|
classNames: props.classNames,
|
|
368
376
|
onChange: setFieldValue
|
|
@@ -372,6 +380,7 @@ function BacanoForm(props) {
|
|
|
372
380
|
FormSections,
|
|
373
381
|
{
|
|
374
382
|
sections: activeStep.sections,
|
|
383
|
+
idPrefix,
|
|
375
384
|
values,
|
|
376
385
|
classNames: props.classNames,
|
|
377
386
|
onChange: setFieldValue
|
|
@@ -382,6 +391,7 @@ function BacanoForm(props) {
|
|
|
382
391
|
FormFields,
|
|
383
392
|
{
|
|
384
393
|
fields,
|
|
394
|
+
idPrefix,
|
|
385
395
|
values,
|
|
386
396
|
classNames: props.classNames,
|
|
387
397
|
onChange: setFieldValue
|
|
@@ -391,6 +401,7 @@ function BacanoForm(props) {
|
|
|
391
401
|
FormSections,
|
|
392
402
|
{
|
|
393
403
|
sections,
|
|
404
|
+
idPrefix,
|
|
394
405
|
values,
|
|
395
406
|
classNames: props.classNames,
|
|
396
407
|
onChange: setFieldValue
|
|
@@ -91,7 +91,12 @@ Figma, desde Bacano Content o desde ambas fuentes.
|
|
|
91
91
|
los contratos específicos del SDK.
|
|
92
92
|
- No usar GraphQL directo, endpoints internos ni lógica comercial duplicada.
|
|
93
93
|
- No codificar textos editables si pertenecen a Bacano Content.
|
|
94
|
-
-
|
|
94
|
+
- Marcar obligatoriamente cada región visual proveniente de un `ContentEntry`
|
|
95
|
+
con `getContentEntryLocatorAttributes(entry)`. El marcador identifica la
|
|
96
|
+
entrada completa, nunca campos individuales. Conservar esta identidad al
|
|
97
|
+
transformar Content en view models y seguir
|
|
98
|
+
`STOREFRONT_CONTENT_LOCATOR.md`.
|
|
99
|
+
- No alterar el storefront de referencia salvo que ese sea el proyecto indicado.
|
|
95
100
|
|
|
96
101
|
Si Content todavía no existe, generar un blueprint de entrega y fixtures
|
|
97
102
|
temporales fieles al contenido visible de Figma. Los fixtures deben usar el
|
|
@@ -147,6 +152,9 @@ proyecto.
|
|
|
147
152
|
Antes de aprobar producción, comprobar que la fuente editorial efectiva sea
|
|
148
153
|
Bacano Content. Un fixture, placeholder o fallback temporal accesible en el
|
|
149
154
|
build de producción es un bloqueo, aunque visualmente coincida con Figma.
|
|
155
|
+
Inspeccionar además el HTML final: toda región alimentada por una entrada debe
|
|
156
|
+
conservar su marcador V1 con la key, nombre y locale correctos. Una región
|
|
157
|
+
editorial sin marcador bloquea la aprobación.
|
|
150
158
|
|
|
151
159
|
Al finalizar, invocar `$bacano-web` si está disponible para la auditoría de
|
|
152
160
|
producción completa. Esta skill valida fidelidad y correcta integración desde
|
|
@@ -7,9 +7,9 @@ negocio.
|
|
|
7
7
|
|
|
8
8
|
Antes de programar, registrar por ruta:
|
|
9
9
|
|
|
10
|
-
| Ruta | Frame | Componente | Fuente | Content | Responsive | Preview | Estado |
|
|
11
|
-
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
12
|
-
| `/` | Home desktop/mobile | `Hero` | Content | `hero_inicio` | 2 frames | activa | pendiente |
|
|
10
|
+
| Ruta | Frame | Componente | Fuente | Content | Localizador | Responsive | Preview | Estado |
|
|
11
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
12
|
+
| `/` | Home desktop/mobile | `Hero` | Content | `hero_inicio` | entrada V1 | 2 frames | activa | pendiente |
|
|
13
13
|
|
|
14
14
|
Fuentes posibles: Content, catálogo estático, live state, public config,
|
|
15
15
|
checkout, auth u otra operación documentada del SDK.
|
|
@@ -18,6 +18,8 @@ checkout, auth u otra operación documentada del SDK.
|
|
|
18
18
|
|
|
19
19
|
- Usar componentes y tokens existentes antes de crear otros.
|
|
20
20
|
- Mapear textos/imágenes editoriales a Content cuando deban ser administrables.
|
|
21
|
+
- Para cada fuente `ContentEntry`, conservar y renderizar el marcador de entrada
|
|
22
|
+
definido en `STOREFRONT_CONTENT_LOCATOR.md`; no bajar a nivel de campo.
|
|
21
23
|
- Si el equipo de Content y desarrollo trabajan en tiempos distintos, seguir
|
|
22
24
|
[content-workflow.md](content-workflow.md) y mantener un único contrato de
|
|
23
25
|
datos entre fixtures temporales y Bacano real.
|
|
@@ -11,6 +11,7 @@ Antes de crear campos o fixtures, asignar cada valor a una fuente:
|
|
|
11
11
|
| Información | Fuente habitual |
|
|
12
12
|
| --- | --- |
|
|
13
13
|
| Títulos, textos, banners y bloques editoriales | Bacano Content |
|
|
14
|
+
| Campos, opciones, pasos y validaciones de formularios | Formularios de Bacano; nunca tipos de Content |
|
|
14
15
|
| Productos, variantes, precios y disponibilidad | Catálogo y live state del SDK |
|
|
15
16
|
| Legal, branding y configuración del sitio | `publicConfig` |
|
|
16
17
|
| Carrito, checkout, cliente y pedidos | Módulo correspondiente del SDK |
|
|
@@ -19,6 +20,12 @@ Antes de crear campos o fixtures, asignar cada valor a una fuente:
|
|
|
19
20
|
No duplicar en Content información que ya tiene una fuente funcional. Un valor
|
|
20
21
|
visible en Figma no implica que sea editorial.
|
|
21
22
|
|
|
23
|
+
Para materializar un formulario del diseño, seguir `STOREFRONT_FORMS.md` y la
|
|
24
|
+
sección de formularios de `CONTENT_TYPE_MANAGEMENT.md`. El cliente temporal
|
|
25
|
+
admite `forms:manage` para crear/editar borradores y `forms:publish` para publicar
|
|
26
|
+
con confirmación. Reutilizar la definición del ERP y sus pares value/label;
|
|
27
|
+
no inventar un esquema paralelo. Los conflictos de revisión requieren releer.
|
|
28
|
+
|
|
22
29
|
## Modelar sin extremos
|
|
23
30
|
|
|
24
31
|
No convertir una página extensa en un único tipo gigante ni crear un componente
|
|
@@ -35,6 +42,10 @@ reduzca duplicación real.
|
|
|
35
42
|
consumidores.
|
|
36
43
|
- No hacer depender la UI del shape crudo de Content: fixture y mapper real
|
|
37
44
|
deben converger en el mismo view model.
|
|
45
|
+
- Conservar en ese view model los atributos producidos por
|
|
46
|
+
`getContentEntryLocatorAttributes(entry)`. Toda región visual administrada
|
|
47
|
+
por una entrada debe exponerlos en su contenedor DOM exterior; no se marcan
|
|
48
|
+
campos individuales.
|
|
38
49
|
|
|
39
50
|
## 2. Elegir el punto de partida
|
|
40
51
|
|
|
@@ -138,6 +149,10 @@ Bacano mapper ─┘
|
|
|
138
149
|
El cambio de fuente debe ocurrir en el loader o repositorio de contenido, nunca
|
|
139
150
|
mediante condicionales repartidos por los componentes.
|
|
140
151
|
|
|
152
|
+
Los fixtures pueden simular el view model, pero no deben inventar marcadores de
|
|
153
|
+
entrada en producción. El marcador definitivo se deriva del `ContentEntry`
|
|
154
|
+
real para que nombre, key y locale no diverjan.
|
|
155
|
+
|
|
141
156
|
## 5. Entrega al equipo de Content
|
|
142
157
|
|
|
143
158
|
Entregar una tabla legible con:
|
|
@@ -167,7 +182,9 @@ Antes de retirar el fixture:
|
|
|
167
182
|
2. Validar campos requeridos, media y contenido largo.
|
|
168
183
|
3. Comparar la página con Figma usando Content real.
|
|
169
184
|
4. Verificar preview si la capacidad está activa.
|
|
170
|
-
5.
|
|
185
|
+
5. Inspeccionar el HTML y confirmar los marcadores definidos en
|
|
186
|
+
`STOREFRONT_CONTENT_LOCATOR.md` para cada región editorial.
|
|
187
|
+
6. Eliminar el fixture del camino de ejecución, no necesariamente de los tests.
|
|
171
188
|
|
|
172
189
|
En producción no se permite fallback silencioso a fixtures. Si una página
|
|
173
190
|
depende de contenido temporal, reportar `❌ NO LISTO` con las rutas y secciones
|
|
@@ -58,6 +58,18 @@ no aceptar una ruta arbitraria solo porque coincide con una cadena. Preview
|
|
|
58
58
|
reutiliza componentes y mappers de producción, permanece fuera del sitemap y
|
|
59
59
|
no habilita funciones transaccionales o marketing.
|
|
60
60
|
|
|
61
|
+
## Trazabilidad editorial
|
|
62
|
+
|
|
63
|
+
Al mapear un `ContentEntry`, conservar los atributos retornados por
|
|
64
|
+
`getContentEntryLocatorAttributes(entry)` hasta el componente visual. Aplicar
|
|
65
|
+
el spread al `section`, `article`, banner o tarjeta exterior que representa la
|
|
66
|
+
entrada. No repetirlo en cada título o párrafo. Si una tarjeta proviene de otra
|
|
67
|
+
entrada referenciada, esa tarjeta lleva su propio marcador anidado.
|
|
68
|
+
|
|
69
|
+
La salida final debe cumplir `STOREFRONT_CONTENT_LOCATOR.md`. Verificar el HTML
|
|
70
|
+
generado; encontrar la llamada al helper en TypeScript no demuestra que el
|
|
71
|
+
marcador haya llegado al DOM.
|
|
72
|
+
|
|
61
73
|
## Diagnóstico antes de cambiar código
|
|
62
74
|
|
|
63
75
|
Un `ORIGIN_NOT_ALLOWED` o un `403` al resolver la configuración durante el
|
|
@@ -47,7 +47,8 @@ Después leer solo:
|
|
|
47
47
|
Si `@bacano/sdk` está disponible, leer sus contratos canónicos aplicables antes
|
|
48
48
|
de proponer cambios. Como mínimo revisar `STOREFRONT_LEGAL.md`,
|
|
49
49
|
`STOREFRONT_MARKETING.md`, `STOREFRONT_PERFORMANCE.md` y
|
|
50
|
-
`STOREFRONT_PREVIEW.md`;
|
|
50
|
+
`STOREFRONT_PREVIEW.md`; si el sitio renderiza Bacano Content, leer también y
|
|
51
|
+
aplicar obligatoriamente `STOREFRONT_CONTENT_LOCATOR.md`; añadir
|
|
51
52
|
`AUTHENTICATION.md`, `STOREFRONT_LEADS.md`, `STOREFRONT_CLARITY.md` y
|
|
52
53
|
otros contratos únicamente cuando esas capacidades existan. Leer
|
|
53
54
|
`STOREFRONT_CONSTRUCTION.md` cuando el website permita ocultarse temporalmente.
|
|
@@ -70,6 +71,8 @@ ejecutar compras/pagos sin autorización explícita.
|
|
|
70
71
|
5. Comparar router, salida del build y sitemap; ninguna ruta pública debe
|
|
71
72
|
omitirse silenciosamente.
|
|
72
73
|
6. Inspeccionar HTML generado, metadata, JSON-LD, media y enlaces internos.
|
|
74
|
+
Si existe Content, comparar las entradas renderizadas con los marcadores V1
|
|
75
|
+
presentes en ese HTML; revisar únicamente el JSX no es evidencia suficiente.
|
|
73
76
|
7. Si existe un dominio o una aplicación ejecutable, auditar sus rutas reales,
|
|
74
77
|
la consola y un trace de red con nombres y conteos de operaciones GraphQL.
|
|
75
78
|
8. Probar vistas representativas desktop y mobile.
|
|
@@ -29,6 +29,13 @@ función apagada en un bloqueo artificial.
|
|
|
29
29
|
- Comprobar que preview, callbacks, cuenta, checkout y respuestas de pago no se
|
|
30
30
|
indexen cuando corresponda.
|
|
31
31
|
- Validar estados vacío, loading, error y ausencia de Content opcional.
|
|
32
|
+
- `P0`: toda región visual cuya fuente sea un `ContentEntry` expone en el HTML
|
|
33
|
+
final el marcador de `STOREFRONT_CONTENT_LOCATOR.md`, con la key, nombre y
|
|
34
|
+
locale de la entrada fuente. El control es por entrada completa, no por campo.
|
|
35
|
+
- Comparar inventario de entradas y regiones. Permitir marcadores repetidos si
|
|
36
|
+
una entrada aparece en varios lugares y marcadores anidados cuando una tarjeta
|
|
37
|
+
proviene de otra entrada referenciada. No exigirlos a código/i18n, catálogo,
|
|
38
|
+
`publicConfig` ni documentos legales.
|
|
32
39
|
|
|
33
40
|
## SEO y compartición
|
|
34
41
|
|
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
Aplicar este contrato cuando el sitio muestre al menos un formulario administrado
|
|
4
4
|
desde Bacano. Leer también `STOREFRONT_FORMS.md` completo desde el paquete SDK.
|
|
5
5
|
|
|
6
|
+
## Creación asistida y sincronización
|
|
7
|
+
|
|
8
|
+
Si falta el formulario, proponer su definición y pedir el acceso temporal de
|
|
9
|
+
**Conectar agente** en Formularios o Content. Leer `CONTENT_TYPE_MANAGEMENT.md`.
|
|
10
|
+
Usar `createContentManagementClient` con `forms:manage`; inventariar sitios,
|
|
11
|
+
listas y formularios antes de crear. Editar mediante `updateForm()` enviando
|
|
12
|
+
el documento completo y `expectedUpdatedAt`. Los conflictos exigen releer,
|
|
13
|
+
no sobreescribir. Publicar o retirar requiere `forms:publish` y confirmación.
|
|
14
|
+
No duplicar las definiciones en Content, GraphQL, SQL ni en fixtures permanentes.
|
|
15
|
+
El storefront consume la key y versión publicada usando el SDK público.
|
|
16
|
+
El acceso administrativo no permite leer respuestas ni PII. `CAPTURE_LEAD`
|
|
17
|
+
ya resuelve la identidad; no ejecutar una segunda captura por campaña.
|
|
18
|
+
|
|
6
19
|
## Implementación oficial
|
|
7
20
|
|
|
8
21
|
- Clasificar formularios como `ACTIVA`, `INACTIVA` o `BLOQUEADA` en la matriz de
|
|
@@ -23,12 +36,25 @@ desde Bacano. Leer también `STOREFRONT_FORMS.md` completo desde el paquete SDK.
|
|
|
23
36
|
|
|
24
37
|
- Enviar mediante `client.forms.submitResponse()` o el envío interno de
|
|
25
38
|
`BacanoForm`.
|
|
39
|
+
- `honeypot` es un campo trampa, no un nonce, timestamp, UUID ni dato del
|
|
40
|
+
visitante. `BacanoForm` lo administra. En un renderer manual, omitirlo salvo
|
|
41
|
+
que exista un input trampa realmente oculto; para una persona siempre debe
|
|
42
|
+
permanecer vacío y el autocompletado no debe llenarlo.
|
|
43
|
+
- `accepted: true` con `responseId: null` es una aceptación silenciosa antispam:
|
|
44
|
+
no demuestra que la respuesta fue guardada. La verificación funcional exige
|
|
45
|
+
un UUID y la respuesta visible una sola vez en Bacano.
|
|
46
|
+
- Los datos de contacto deben conservar su semántica: usar campos `email` y
|
|
47
|
+
`phone`. Un campo `text` llamado correo o teléfono se guarda como texto libre
|
|
48
|
+
y no alimenta los datos de contacto normalizados para CRM o automatizaciones.
|
|
26
49
|
- Conservar una `idempotencyKey` durante reintentos del mismo intento lógico.
|
|
27
50
|
- Enviar la versión y `definitionHash` exactos de la definición renderizada.
|
|
28
51
|
- Respetar estados `ACTIVE` y `PAUSED`; no renderizar borradores o archivados.
|
|
29
52
|
- No escribir respuestas o PII en consola, analítica, URLs o logs del storefront.
|
|
30
|
-
-
|
|
31
|
-
|
|
53
|
+
- En `CAPTURE_LEAD`, Bacano conserva primero la respuesta y resuelve la identidad
|
|
54
|
+
internamente. No afirmar al visitante que se creó un lead o que ya era cliente:
|
|
55
|
+
la respuesta pública es genérica y no expone ese resultado.
|
|
56
|
+
- No afirmar que se envió un correo, se entregó un cupón o se abrió una solicitud
|
|
57
|
+
si esa acción aún no está implementada y verificada en Bacano.
|
|
32
58
|
- Una brecha del SDK se reporta; nunca se resuelve con GraphQL directo ni una API
|
|
33
59
|
paralela del storefront.
|
|
34
60
|
|
|
@@ -41,6 +67,10 @@ desde Bacano. Leer también `STOREFRONT_FORMS.md` completo desde el paquete SDK.
|
|
|
41
67
|
- Error de red: estado recuperable sin promesa falsa de recepción.
|
|
42
68
|
- Persistencia: la respuesta aparece una sola vez en Bacano con versión, origen y
|
|
43
69
|
snapshot correctos.
|
|
70
|
+
- Autocompletado: completar datos con el navegador o un gestor de contraseñas no
|
|
71
|
+
llena el honeypot ni produce `responseId: null`.
|
|
72
|
+
- Semántica: correo y teléfono usan `email` y `phone`; no se infieren por label o
|
|
73
|
+
key.
|
|
44
74
|
- Accesibilidad: label asociado, foco, teclado, mensajes de error y progreso de
|
|
45
75
|
pasos comprensibles.
|
|
46
76
|
- Static First: el HTML exportado conserva el formulario cuando corresponde.
|
package/types.d.ts
CHANGED
|
@@ -224,11 +224,15 @@ export interface WebsiteFormResponseInput {
|
|
|
224
224
|
idempotencyKey: string;
|
|
225
225
|
values: Record<string, WebsiteFormResponseValue>;
|
|
226
226
|
source?: WebsiteFormResponseSource;
|
|
227
|
-
/**
|
|
227
|
+
/**
|
|
228
|
+
* Bot-trap field, not a token or nonce. Omit it unless a real hidden trap is
|
|
229
|
+
* rendered; legitimate visitors must always submit an empty value.
|
|
230
|
+
*/
|
|
228
231
|
honeypot?: string;
|
|
229
232
|
}
|
|
230
233
|
export interface WebsiteFormResponseResult {
|
|
231
234
|
accepted: boolean;
|
|
235
|
+
/** A null value is not proof of persistence (for example, an anti-bot response). */
|
|
232
236
|
responseId: string | null;
|
|
233
237
|
message: string;
|
|
234
238
|
}
|