@bacano/sdk 1.1.12 → 1.1.15

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.
@@ -33,6 +33,34 @@ function encodeIdentifier(identifier) {
33
33
  }
34
34
  return encodeURIComponent(normalized);
35
35
  }
36
+ function entryListPath(input = {}) {
37
+ const search = new URLSearchParams();
38
+ if (input.contentType) search.set("contentType", input.contentType);
39
+ if (input.locale) search.set("locale", input.locale);
40
+ if (input.status) search.set("status", input.status);
41
+ if (input.limit !== void 0) search.set("limit", String(input.limit));
42
+ if (input.offset !== void 0) search.set("offset", String(input.offset));
43
+ const query = search.toString();
44
+ return query ? `/entries?${query}` : "/entries";
45
+ }
46
+ function mediaListPath(input = {}) {
47
+ const search = new URLSearchParams();
48
+ if (input.search) search.set("search", input.search);
49
+ if (input.limit !== void 0) search.set("limit", String(input.limit));
50
+ if (input.offset !== void 0) search.set("offset", String(input.offset));
51
+ const query = search.toString();
52
+ return query ? `/media?${query}` : "/media";
53
+ }
54
+ function uploadBody(file) {
55
+ if ("arrayBuffer" in file) return file;
56
+ if (file instanceof ArrayBuffer) return file;
57
+ const bytes = new Uint8Array(file.byteLength);
58
+ bytes.set(file);
59
+ return bytes.buffer;
60
+ }
61
+ function encodedHeader(value) {
62
+ return encodeURIComponent(value.trim());
63
+ }
36
64
  function createContentManagementClient(config) {
37
65
  const apiUrl = normalizeApiUrl(config.apiUrl);
38
66
  const fetchImplementation = config.fetch ?? globalThis.fetch;
@@ -63,7 +91,7 @@ function createContentManagementClient(config) {
63
91
  headers: {
64
92
  Accept: "application/json",
65
93
  Authorization: `Bearer ${await getAccessToken()}`,
66
- ...init?.body ? { "Content-Type": "application/json" } : {},
94
+ ...typeof init?.body === "string" ? { "Content-Type": "application/json" } : {},
67
95
  ...init?.headers
68
96
  }
69
97
  }
@@ -84,8 +112,8 @@ function createContentManagementClient(config) {
84
112
  }
85
113
  return payload.data;
86
114
  };
87
- const lifecycle = (identifier, operation, dryRun) => request(
88
- `/types/${encodeIdentifier(identifier)}/lifecycle`,
115
+ const lifecycle = (resource, identifier, operation, dryRun) => request(
116
+ `/${resource}/${encodeIdentifier(identifier)}/lifecycle`,
89
117
  {
90
118
  method: "POST",
91
119
  body: JSON.stringify({ operation, dryRun })
@@ -104,8 +132,82 @@ function createContentManagementClient(config) {
104
132
  method: "PUT",
105
133
  body: JSON.stringify(input)
106
134
  }),
107
- inspectLifecycle: (identifier, operation) => lifecycle(identifier, operation, true),
108
- applyLifecycle: (identifier, operation) => lifecycle(identifier, operation, false)
135
+ inspectLifecycle: (identifier, operation) => lifecycle("types", identifier, operation, true),
136
+ applyLifecycle: (identifier, operation) => lifecycle("types", identifier, operation, false),
137
+ listEntries: (input) => request(entryListPath(input)),
138
+ getEntry: (identifier, locale) => {
139
+ const query = locale ? `?locale=${encodeURIComponent(locale.trim())}` : "";
140
+ return request(
141
+ `/entries/${encodeIdentifier(identifier)}${query}`
142
+ );
143
+ },
144
+ createEntry: (input) => request("/entries", {
145
+ method: "POST",
146
+ body: JSON.stringify(input)
147
+ }),
148
+ updateEntry: (identifier, input) => request(
149
+ `/entries/${encodeIdentifier(identifier)}`,
150
+ {
151
+ method: "PUT",
152
+ body: JSON.stringify(input)
153
+ }
154
+ ),
155
+ inspectEntryLifecycle: (identifier, operation) => lifecycle("entries", identifier, operation, true),
156
+ applyEntryLifecycle: (identifier, operation) => lifecycle("entries", identifier, operation, false),
157
+ listMedia: (input) => request(mediaListPath(input)),
158
+ uploadImage: (input) => {
159
+ const fileName = input.fileName.trim();
160
+ if (!fileName) {
161
+ throw new ContentManagementError({
162
+ message: "A descriptive image file name is required.",
163
+ code: "CONTENT_MEDIA_FILE_NAME_REQUIRED",
164
+ status: 0
165
+ });
166
+ }
167
+ return request("/media/images", {
168
+ method: "POST",
169
+ headers: {
170
+ "Content-Type": "application/octet-stream",
171
+ "X-Bacano-File-Name": encodedHeader(fileName),
172
+ ...input.altText ? { "X-Bacano-Alt-Text": encodedHeader(input.altText) } : {}
173
+ },
174
+ body: uploadBody(input.file)
175
+ });
176
+ },
177
+ listGroups: () => request("/groups"),
178
+ getGroup: (identifier) => request(
179
+ `/groups/${encodeIdentifier(identifier)}`
180
+ ),
181
+ createGroup: (input) => request("/groups", {
182
+ method: "POST",
183
+ body: JSON.stringify(input)
184
+ }),
185
+ updateGroup: (identifier, input) => request(
186
+ `/groups/${encodeIdentifier(identifier)}`,
187
+ {
188
+ method: "PUT",
189
+ body: JSON.stringify(input)
190
+ }
191
+ ),
192
+ inspectGroupLifecycle: (identifier, operation) => lifecycle("groups", identifier, operation, true),
193
+ applyGroupLifecycle: (identifier, operation) => lifecycle("groups", identifier, operation, false),
194
+ listOptionLists: () => request("/option-lists"),
195
+ getOptionList: (identifier) => request(
196
+ `/option-lists/${encodeIdentifier(identifier)}`
197
+ ),
198
+ createOptionList: (input) => request("/option-lists", {
199
+ method: "POST",
200
+ body: JSON.stringify(input)
201
+ }),
202
+ updateOptionList: (identifier, input) => request(
203
+ `/option-lists/${encodeIdentifier(identifier)}`,
204
+ {
205
+ method: "PUT",
206
+ body: JSON.stringify(input)
207
+ }
208
+ ),
209
+ inspectOptionListLifecycle: (identifier, operation) => lifecycle("option-lists", identifier, operation, true),
210
+ applyOptionListLifecycle: (identifier, operation) => lifecycle("option-lists", identifier, operation, false)
109
211
  };
110
212
  }
111
213
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacano/sdk",
3
- "version": "1.1.12",
3
+ "version": "1.1.15",
4
4
  "description": "TypeScript SDK for building websites on the Bacano ERP",
5
5
  "main": "./index.cjs",
6
6
  "module": "./index.js",
@@ -13,7 +13,9 @@ autenticación, marketing o Content por fuera de `@bacano/sdk`.
13
13
 
14
14
  Antes de extraer el diseño, identificar:
15
15
 
16
- - Web informativa o tienda virtual.
16
+ - Web informativa, tienda virtual o sitio híbrido. Si permite una compra
17
+ completa, aplicar también los contratos de tienda a su superficie
18
+ transaccional.
17
19
  - Enlace al archivo, página, sección o frame de Figma.
18
20
  - Rutas que se implementarán y sus frames desktop/mobile.
19
21
  - Repositorio y cascarón Bacano que se reutilizarán.
@@ -74,11 +76,13 @@ pnpm exec bacano-figma-web validate-snapshot .bacano/designs/<proyecto>/manifest
74
76
 
75
77
  ## 4. Mapear a Bacano antes de programar
76
78
 
77
- Leer [references/bacano-mapping.md](references/bacano-mapping.md) y
78
- [references/content-workflow.md](references/content-workflow.md). Preparar una
79
- matriz ruta → frame → componente → fuente de datos → estado responsive y
80
- determinar si el trabajo comienza desde Figma, desde Bacano Content o desde
81
- ambas fuentes.
79
+ Leer [references/bacano-mapping.md](references/bacano-mapping.md),
80
+ [references/content-workflow.md](references/content-workflow.md),
81
+ [references/content-media.md](references/content-media.md) y
82
+ [references/implementation-quality.md](references/implementation-quality.md).
83
+ Preparar una matriz ruta → frame → componente → fuente de datos → Content →
84
+ responsive → preview → estado, y determinar si el trabajo comienza desde
85
+ Figma, desde Bacano Content o desde ambas fuentes.
82
86
 
83
87
  - Reutilizar el starter, tokens y componentes existentes.
84
88
  - Consumir Bacano y Content mediante `@bacano/sdk`.
@@ -96,9 +100,19 @@ nunca repartir datos de prueba dentro de la UI. Si Content ya existe, mapearlo
96
100
  sin sobrescribir estructuras o entradas. Si el usuario pide materializar el
97
101
  blueprint, leer la sección de automatización de
98
102
  [references/content-workflow.md](references/content-workflow.md) y el contrato
99
- `CONTENT_TYPE_MANAGEMENT.md` incluido en `@bacano/sdk`. Solo crear o modificar
100
- tipos y componentes mediante el acceso temporal oficial; las entradas y media
101
- permanecen en el backoffice.
103
+ `CONTENT_TYPE_MANAGEMENT.md` incluido en `@bacano/sdk`. Crear o modificar tipos,
104
+ componentes, grupos, listas y entradas únicamente mediante el acceso temporal
105
+ y los scopes oficiales. Inventariar esos recursos antes de crear otros y
106
+ preparar entradas en borrador por defecto; la media permanece en el
107
+ backoffice salvo que el usuario conceda `content_media:upload`. Con ese scope,
108
+ buscar primero imágenes reutilizables y cargar bytes únicamente mediante
109
+ `listMedia()` y `uploadImage()`; publicar requiere permiso y aprobación
110
+ explícitos.
111
+
112
+ Al proponer keys nuevas, resolver primero el idioma según
113
+ `CONTENT_TYPE_MANAGEMENT.md`: instrucción explícita, convención existente y
114
+ después idioma principal del website. No traducir ni renombrar contratos ya
115
+ utilizados por el storefront.
102
116
 
103
117
  Antes de modificar código, indicar brevemente qué rutas y componentes se
104
118
  implementarán y qué decisiones de diseño son inferidas.
@@ -113,9 +127,15 @@ Trabajar por página o conjunto pequeño de secciones:
113
127
  4. Integración Bacano real.
114
128
  5. Comparación visual y correcciones.
115
129
 
130
+ No dar una página por validada únicamente porque compila. Comparar estructura,
131
+ contenido, responsive e interacciones del corte antes de continuar y registrar
132
+ lo pendiente cuando la revisión visual quede a cargo del usuario.
133
+
116
134
  No añadir paquetes, fuentes, iconos o assets si el proyecto ya ofrece un
117
135
  equivalente. Descargar assets mediante la fuente Figma autorizada; no incrustar
118
- URLs temporales ni construir transformaciones de Storage manualmente.
136
+ URLs temporales ni construir transformaciones de Storage manualmente. La carga
137
+ administrativa admite JPEG, PNG o WebP raster; video, documentos, SVG y
138
+ animaciones continúan bajo revisión editorial en el backoffice.
119
139
 
120
140
  ## 6. Validar diseño y contrato
121
141
 
@@ -7,9 +7,9 @@ negocio.
7
7
 
8
8
  Antes de programar, registrar por ruta:
9
9
 
10
- | Ruta | Frame | Componente | Fuente | Responsive | Estado |
11
- | --- | --- | --- | --- | --- | --- |
12
- | `/` | Home desktop/mobile | `Hero` | Content | 2 frames | pendiente |
10
+ | Ruta | Frame | Componente | Fuente | Content | Responsive | Preview | Estado |
11
+ | --- | --- | --- | --- | --- | --- | --- | --- |
12
+ | `/` | Home desktop/mobile | `Hero` | Content | `hero_inicio` | 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.
@@ -0,0 +1,57 @@
1
+ # Imágenes de Content desde un agente
2
+
3
+ Usar este flujo solo cuando `getContext().scopes` incluya
4
+ `content_media:upload`. El token temporal continúa siendo administrativo: no se
5
+ incluye en el storefront, Figma, archivos del proyecto, logs ni respuestas al
6
+ usuario.
7
+
8
+ ## Orden obligatorio
9
+
10
+ 1. Clasificar el asset: hero desktop/mobile, card, galería, logo, decorativo u
11
+ otro uso editorial.
12
+ 2. Buscar en `listMedia()` con términos semánticos. Reutilizar únicamente cuando
13
+ sea el mismo asset, no solo una imagen parecida.
14
+ 3. Exportar desde la fuente Figma autorizada los bytes originales necesarios.
15
+ 4. Para imágenes informativas, preparar un nombre descriptivo y `altText`
16
+ contextual. No usar nombres de capas como `Frame 32` o `image_1`.
17
+ 5. Cargar con `uploadImage()`. Nunca usar GraphQL, URLs de Figma, base64 dentro
18
+ de una entrada ni endpoints directos de Nhost Storage.
19
+ 6. Guardar en Content el `fileId` devuelto y volver a consultar la entrada.
20
+ 7. Mantener en borrador hasta validar imagen, encuadre, responsive y texto
21
+ alternativo en contexto.
22
+
23
+ ```ts
24
+ import { readFile } from "node:fs/promises";
25
+ import { createContentManagementClient } from "@bacano/sdk/content-management";
26
+
27
+ const content = createContentManagementClient({
28
+ apiUrl: process.env.BACANO_API_URL!,
29
+ accessToken: process.env.BACANO_CONTENT_ACCESS_TOKEN!,
30
+ });
31
+
32
+ const matches = await content.listMedia({ search: "hero juguetes" });
33
+ const media = matches.items[0] ?? await content.uploadImage({
34
+ file: await readFile("./exports/hero-juguetes-desktop.png"),
35
+ fileName: "Hero juguetes para bebé desktop",
36
+ altText: "Niña jugando con bloques de colores",
37
+ });
38
+
39
+ console.log(media.fileId);
40
+ ```
41
+
42
+ ## Contrato de la primera versión
43
+
44
+ - Admite JPEG, PNG y WebP raster no animado.
45
+ - Máximo 10 MB de entrada y 100 megapíxeles.
46
+ - Bacano valida bytes reales, corrige orientación y normaliza a WebP.
47
+ - El nombre se convierte a un filename SEO seguro.
48
+ - Los duplicados binarios normalizados se reutilizan dentro de la misma
49
+ empresa; nunca entre empresas.
50
+ - SVG, GIF animado, video y documentos se cargan desde el backoffice.
51
+ - El agente no reemplaza ni elimina físicamente media.
52
+ - `altText` de biblioteca es fallback. El texto alternativo final pertenece al
53
+ uso dentro de la entrada; una imagen decorativa usa `alt=""` en ese contexto.
54
+
55
+ Si el asset no cabe en este contrato, reportar `BLOQUEADO` y pedir que el equipo
56
+ editorial lo prepare en la biblioteca. No convertirlo de forma improvisada ni
57
+ introducir paquetes o servicios paralelos.
@@ -19,12 +19,30 @@ Antes de crear campos o fixtures, asignar cada valor a una fuente:
19
19
  No duplicar en Content información que ya tiene una fuente funcional. Un valor
20
20
  visible en Figma no implica que sea editorial.
21
21
 
22
+ ## Modelar sin extremos
23
+
24
+ No convertir una página extensa en un único tipo gigante ni crear un componente
25
+ para cada bloque visual. Separar una sección cuando tenga ciclo editorial,
26
+ responsable, reutilización o repetición propios. Crear un componente compartido
27
+ solo cuando coincidan tanto la estructura como el significado y la abstracción
28
+ reduzca duplicación real.
29
+
30
+ - Usar grupos para organizar entradas relacionadas por dominio o página.
31
+ - Usar repetibles para pasos, integrantes, preguntas, tarjetas y otras
32
+ colecciones homogéneas.
33
+ - Mantener directamente en el tipo los campos de un bloque exclusivo y simple.
34
+ - Crear primero listas y componentes realmente compartidos; después los tipos
35
+ consumidores.
36
+ - No hacer depender la UI del shape crudo de Content: fixture y mapper real
37
+ deben converger en el mismo view model.
38
+
22
39
  ## 2. Elegir el punto de partida
23
40
 
24
41
  ### Figma primero
25
42
 
26
43
  1. Extraer el contenido público visible y sus assets autorizados.
27
- 2. Proponer tipos, keys, campos y entradas en `content-blueprint.json`.
44
+ 2. Resolver el idioma y proponer tipos, keys, campos y entradas en
45
+ `content-blueprint.json` siguiendo `CONTENT_TYPE_MANAGEMENT.md`.
28
46
  3. Crear fixtures locales que implementen el view model de la página.
29
47
  4. Implementar y validar el diseño contra esos fixtures.
30
48
  5. Entregar el blueprint al equipo de Content para crearlo en Bacano.
@@ -66,22 +84,22 @@ Estructura recomendada:
66
84
  "route": "/",
67
85
  "sections": [
68
86
  {
69
- "sectionKey": "home-hero",
87
+ "sectionKey": "hero-inicio",
70
88
  "figmaNodeId": "1:20",
71
89
  "component": "HomeHero",
72
90
  "ownership": "content",
73
- "contentTypeKey": "home_hero",
74
- "entryKey": "home-main-hero",
91
+ "contentTypeKey": "hero_inicio",
92
+ "entryKey": "hero-principal-inicio",
75
93
  "status": "missing",
76
94
  "fields": [
77
95
  {
78
- "key": "title",
96
+ "key": "titulo",
79
97
  "type": "text",
80
98
  "required": true,
81
99
  "sampleValue": "Compra para cada etapa"
82
100
  }
83
101
  ],
84
- "fixture": "fixtures/home-hero.json"
102
+ "fixture": "fixtures/hero-inicio.json"
85
103
  }
86
104
  ]
87
105
  }
@@ -92,7 +110,9 @@ Estructura recomendada:
92
110
  El blueprint es una propuesta y una evidencia de coordinación, no una orden de
93
111
  escritura. No incluir IDs internos innecesarios, secretos, datos personales ni
94
112
  respuestas crudas de GraphQL. Mantener keys estables y registrar cambios que
95
- rompan el contrato.
113
+ rompan el contrato. Las propiedades técnicas del formato del blueprint
114
+ permanecen en inglés; los valores que representan tipos, componentes, campos y
115
+ entradas definidos por la empresa siguen el idioma resuelto para el proyecto.
96
116
 
97
117
  ## 4. Fixtures temporales
98
118
 
@@ -132,9 +152,12 @@ Entregar una tabla legible con:
132
152
  El equipo de Content puede crear o ajustar la estructura desde Bacano. Si el
133
153
  usuario autoriza que el agente materialice el blueprint, usar exclusivamente
134
154
  `@bacano/sdk/content-management` y el token temporal generado desde **Conectar
135
- agente**. Confirmar la empresa, inventariar primero, crear componentes antes de
136
- sus consumidores y trabajar inicialmente en borrador. La skill no crea entradas,
137
- importaciones ni cargas de media.
155
+ agente**. Confirmar la empresa y los scopes, inventariar primero, crear
156
+ componentes antes de sus consumidores y trabajar inicialmente en borrador. La
157
+ skill puede preparar entradas cuando el usuario conceda
158
+ `content_entries:manage`. Si además se concede `content_media:upload`, debe
159
+ buscar y cargar imágenes por el contrato descrito en
160
+ [content-media.md](content-media.md). No publica sin el scope específico.
138
161
 
139
162
  ## 6. Cambio a Bacano real
140
163
 
@@ -159,11 +182,24 @@ escribir:
159
182
 
160
183
  1. Pedir al usuario que genere un acceso temporal de 15, 30 o 60 minutos.
161
184
  2. Guardarlo solo en el entorno local del proceso del agente.
162
- 3. Ejecutar `getContext()`, `listTypes()` y `getReferences()`.
185
+ 3. Ejecutar `getContext()`, comprobar sus `scopes`, y después `listTypes()`,
186
+ `listGroups()`, `listOptionLists()` y `getReferences()`.
163
187
  4. Mostrar el plan y esperar aprobación cuando altere estructuras existentes.
164
- 5. Crear primero componentes y luego tipos consumidores, siempre en borrador.
165
- 6. Para archivar o eliminar, inspeccionar dependencias y confirmar nuevamente.
188
+ 5. Crear primero listas y componentes, luego tipos consumidores, y mantener las
189
+ estructuras nuevas en borrador hasta validarlas.
190
+ 6. Si se concedió `content_entries:manage`, inventariar entradas por tipo, key y
191
+ locale; crear las faltantes en borrador y actualizar únicamente con
192
+ `expectedUpdatedAt`.
193
+ 7. No fusionar por nombres, no usar HTML crudo y no relacionar URLs remotas. La
194
+ media debe existir en Bacano y pertenecer a la empresa autorizada; resolver
195
+ su `fileId` mediante `listMedia()` o `uploadImage()`.
196
+ 8. Para archivar o eliminar tipos, entradas, grupos o listas, inspeccionar
197
+ dependencias y confirmar nuevamente. Una actualización de lista debe enviar
198
+ todos los elementos que deben permanecer, incluidos sus IDs existentes.
199
+ 9. Publicar o modificar una entrada publicada solo con
200
+ `content_entries:publish` y una aprobación humana explícita.
166
201
 
167
202
  Nunca exponer el token en el navegador, commit, blueprint, salida de consola o
168
203
  chat. Nunca convertir el blueprint en una importación implícita ni usar GraphQL
169
- directo. Las entradas y media permanecen bajo gestión editorial en Bacano.
204
+ directo. Sin `content_media:upload`, toda media permanece bajo gestión
205
+ editorial en el backoffice.
@@ -87,6 +87,9 @@ Estado de página: `pending`, `extracted`, `implemented` o `validated`.
87
87
 
88
88
  Cada página requiere `route`, `name`, `status` y al menos un frame desktop o
89
89
  mobile con `nodeId` o `url`. No guardar secretos ni respuestas MCP completas.
90
+ Antes de implementar, comprobar que las rutas no apuntan accidentalmente al
91
+ mismo frame o `nodeId`. Una reutilización intencional debe quedar registrada en
92
+ `assumptions`.
90
93
 
91
94
  ## Estrategia de consultas
92
95
 
@@ -0,0 +1,67 @@
1
+ # Calidad de implementación desde Figma
2
+
3
+ Aplicar estas reglas al construir la interfaz. Son criterios de resultado, no
4
+ una obligación de usar una librería concreta.
5
+
6
+ ## Responsive e inferencias
7
+
8
+ Si Figma no incluye un frame móvil, construir una adaptación mobile first y
9
+ registrarla como inferencia. Conservar jerarquía, contenido, orden semántico y
10
+ acciones principales; apilar columnas cuando corresponda y evitar overflow
11
+ horizontal. Verificar al menos móvil, tablet y escritorio.
12
+
13
+ No convertir automáticamente toda colección en carrusel. Usarlo únicamente
14
+ cuando mejore la exploración y asegurar scroll táctil, teclado, controles
15
+ accesibles, dimensiones estables y pausa durante la interacción. Respetar
16
+ `prefers-reduced-motion` cuando exista autoplay o movimiento.
17
+
18
+ ## Componentes e interacción
19
+
20
+ Inspeccionar dependencias y componentes del proyecto antes de añadir paquetes.
21
+ No imponer HeroUI, Embla, Motion, CSS Modules u otra librería; reutilizar el
22
+ sistema existente y pedir autorización antes de introducir una dependencia
23
+ visual relevante.
24
+
25
+ Todo control interactivo necesita estado de hover cuando aplique,
26
+ `focus-visible`, teclado, contraste y área táctil suficiente. Usar
27
+ `aria-current="page"` en la navegación activa y no comunicar estados solo con
28
+ color. Las animaciones no deben cambiar el layout final, dejar contenido útil
29
+ oculto sin JavaScript ni ejecutarse contra la preferencia de movimiento
30
+ reducido.
31
+
32
+ ## Perfil de exportación estática
33
+
34
+ Cuando el hosting y el proyecto estén definidos como export estático, comprobar
35
+ la configuración efectiva del framework. En Next.js suele incluir
36
+ `output: "export"`, `trailingSlash: true` y una estrategia de imágenes
37
+ compatible con exportación estática.
38
+
39
+ Materializar durante el build contenido, rutas, metadata, sitemap y JSON-LD.
40
+ Las rutas dinámicas editoriales deben enumerar todas las entradas publicadas y
41
+ fallar de forma explícita si el snapshot esperado está vacío o incompleto.
42
+
43
+ No introducir Route Handlers, API Routes, Server Actions, middleware, ISR,
44
+ `force-dynamic` ni secretos de servidor en un proyecto que debe producir solo
45
+ archivos estáticos. Esta prohibición no aplica automáticamente a un storefront
46
+ híbrido o SSR: primero clasificar su arquitectura.
47
+
48
+ Mantener un único bootstrap de configuración pública y un cliente Bacano por
49
+ worker o árbol. Reutilizar esa misma información para páginas, metadata,
50
+ sitemap, JSON-LD y provider; no consultar cada ruta por separado si existe un
51
+ snapshot oficial compartido.
52
+
53
+ ## Preview y rutas editoriales
54
+
55
+ Mantener una allowlist finita de patrones editoriales. Los índices pueden ser
56
+ explícitos y los slugs de detalle pueden derivarse de las entradas publicadas;
57
+ no aceptar una ruta arbitraria solo porque coincide con una cadena. Preview
58
+ reutiliza componentes y mappers de producción, permanece fuera del sitemap y
59
+ no habilita funciones transaccionales o marketing.
60
+
61
+ ## Diagnóstico antes de cambiar código
62
+
63
+ Un `ORIGIN_NOT_ALLOWED` o un `403` al resolver la configuración durante el
64
+ build suele indicar que el origen utilizado no está autorizado para el website.
65
+ Verificar dominio canónico, orígenes permitidos y ambiente antes de alterar el
66
+ cliente SDK. Una advertencia aislada sobre ausencia de caché del build no prueba
67
+ por sí sola que el storefront esté defectuoso.
@@ -2,6 +2,10 @@
2
2
 
3
3
  Validar por cortes para evitar corregir 70 secciones al final.
4
4
 
5
+ Para cada corte, distinguir `implementado`, `validado`, `pendiente de revisión
6
+ humana`, `no aplica` y `bloqueado`. Si el usuario conserva la validación visual,
7
+ no convertir su ausencia en una aprobación automática.
8
+
5
9
  ## Evidencia mínima por página
6
10
 
7
11
  - URL/ruta implementada.