@bacano/sdk 1.1.13 → 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.
- package/CONTENT_TYPE_MANAGEMENT.md +54 -8
- package/README.md +5 -4
- package/content-management-types.d.ts +31 -0
- package/content-management.cjs +39 -1
- package/content-management.js +39 -1
- package/package.json +1 -1
- package/skills/bacano-figma-web/SKILL.md +9 -3
- package/skills/bacano-figma-web/references/content-media.md +57 -0
- package/skills/bacano-figma-web/references/content-workflow.md +7 -4
|
@@ -2,9 +2,9 @@
|
|
|
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
|
|
6
|
-
autoriza productos, ventas, clientes,
|
|
7
|
-
Storage.
|
|
5
|
+
de opciones, entradas e imágenes** de Bacano Content. Cada capacidad requiere
|
|
6
|
+
un permiso explícito. El acceso no autoriza productos, ventas, clientes,
|
|
7
|
+
configuración general, GraphQL, SQL ni acceso directo a Storage.
|
|
8
8
|
|
|
9
9
|
Este contrato es independiente del cliente público del storefront. Debe usarse
|
|
10
10
|
solo desde un proceso local o servidor confiable; nunca desde el navegador ni
|
|
@@ -30,8 +30,8 @@ Scopes disponibles:
|
|
|
30
30
|
- `content_entries:manage`: grupos, entradas en borrador y sus ciclos de vida.
|
|
31
31
|
- `content_entries:publish`: publicar y modificar contenido publicado; requiere
|
|
32
32
|
también `content_entries:manage`.
|
|
33
|
-
- `content_media:upload`:
|
|
34
|
-
|
|
33
|
+
- `content_media:upload`: consultar imágenes de la empresa y cargar JPEG, PNG o
|
|
34
|
+
WebP. Requiere también `content_entries:manage`; no permite borrar archivos.
|
|
35
35
|
|
|
36
36
|
No guardes el token en Git, Figma, Content, logs, variables públicas ni archivos
|
|
37
37
|
que se empaqueten con el storefront.
|
|
@@ -77,6 +77,8 @@ El cliente ofrece:
|
|
|
77
77
|
`updateOptionList(idOrKey, input)`.
|
|
78
78
|
- `inspectOptionListLifecycle(idOrKey, operation)` y
|
|
79
79
|
`applyOptionListLifecycle(idOrKey, operation)`.
|
|
80
|
+
- `listMedia(filters)` y `uploadImage(input)` para reutilizar o cargar imágenes
|
|
81
|
+
cuando el acceso incluya `content_media:upload`.
|
|
80
82
|
|
|
81
83
|
## Idioma, nombres y keys
|
|
82
84
|
|
|
@@ -317,8 +319,9 @@ Reglas obligatorias:
|
|
|
317
319
|
y presentar el cambio; no se sobrescribe silenciosamente.
|
|
318
320
|
3. Enviar el documento completo que debe permanecer. `updateEntry()` reemplaza
|
|
319
321
|
el contenido de la entrada después de aplicar las validaciones oficiales.
|
|
320
|
-
4. Reutilizar únicamente `fileId`
|
|
321
|
-
|
|
322
|
+
4. Reutilizar únicamente `fileId` pertenecientes a la misma empresa. Con
|
|
323
|
+
`content_media:upload`, resolverlos mediante `listMedia()` o
|
|
324
|
+
`uploadImage()`; nunca relacionar URLs arbitrarias.
|
|
322
325
|
5. Rich text usa el JSON controlado por Bacano, nunca HTML crudo.
|
|
323
326
|
6. Publicar, retirar o modificar una entrada publicada requiere
|
|
324
327
|
`content_entries:publish` y aprobación humana explícita.
|
|
@@ -328,6 +331,45 @@ La API reutiliza el mismo servicio de guardado del backoffice: campos
|
|
|
328
331
|
obligatorios, componentes anidados, repetibles, listas, referencias, rich text,
|
|
329
332
|
media y pertenencia a la empresa se validan de forma autoritativa.
|
|
330
333
|
|
|
334
|
+
## Imágenes
|
|
335
|
+
|
|
336
|
+
Antes de cargar, busca por nombre o texto alternativo para reutilizar la
|
|
337
|
+
biblioteca existente:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import { readFile } from "node:fs/promises";
|
|
341
|
+
|
|
342
|
+
const existing = await content.listMedia({ search: "hero juguetes" });
|
|
343
|
+
|
|
344
|
+
const hero = existing.items[0] ?? await content.uploadImage({
|
|
345
|
+
file: await readFile("./hero-juguetes.png"),
|
|
346
|
+
fileName: "Hero juguetes para bebé",
|
|
347
|
+
altText: "Niña jugando con juguetes educativos",
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
console.log(hero.fileId, hero.width, hero.height);
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
`uploadImage()` envía los bytes al endpoint administrativo de Bacano. El
|
|
354
|
+
servidor comprueba el contenido real, corrige orientación, elimina metadatos no
|
|
355
|
+
necesarios, normaliza a WebP y registra nombre, MIME, tamaño y dimensiones. El
|
|
356
|
+
límite de entrada es 10 MB y 100 megapíxeles. No se admiten animaciones,
|
|
357
|
+
archivos corruptos, SVG, video ni documentos en esta primera versión.
|
|
358
|
+
|
|
359
|
+
El nombre entregado debe describir el asset; Bacano lo normaliza a un filename
|
|
360
|
+
SEO seguro y agrega un sufijo corto para evitar colisiones. Un SHA-256 de los
|
|
361
|
+
bytes WebP normalizados permite reutilizar duplicados dentro de la misma
|
|
362
|
+
empresa. Nunca se deduplica entre empresas.
|
|
363
|
+
|
|
364
|
+
El `altText` de la biblioteca es solo un fallback. Al asociar una imagen a una
|
|
365
|
+
entrada, conserva el texto alternativo contextual que corresponda a ese uso;
|
|
366
|
+
una misma imagen puede significar cosas distintas en dos secciones. Para una
|
|
367
|
+
imagen decorativa el contenido final debe usar `alt=""`.
|
|
368
|
+
|
|
369
|
+
El agente no puede reemplazar ni borrar físicamente media. Una edición genera
|
|
370
|
+
otro asset y cambia explícitamente el `fileId` de la entrada. Esto evita romper
|
|
371
|
+
productos, servicios, branding u otras entradas que todavía usen el archivo.
|
|
372
|
+
|
|
331
373
|
## Grupos y listas de opciones
|
|
332
374
|
|
|
333
375
|
Los grupos organizan entradas y usan el scope `content_entries:manage`. Las
|
|
@@ -385,12 +427,16 @@ omitas opciones existentes por accidente en una actualización.
|
|
|
385
427
|
- Emisión, revocación y mutaciones quedan en una auditoría inmutable sin guardar
|
|
386
428
|
el bearer token ni el documento completo.
|
|
387
429
|
- Las respuestas del endpoint usan `no-store`, límites de tamaño y rate limit.
|
|
430
|
+
- Las cargas tienen un límite adicional por concesión; no aceptan importación
|
|
431
|
+
por URL ni exponen credenciales o endpoints internos de Storage.
|
|
388
432
|
- Los tokens v2 no son compatibles con tokens anteriores y tienen como máximo
|
|
389
433
|
60 minutos de vigencia.
|
|
390
434
|
|
|
391
435
|
## Límites de esta versión
|
|
392
436
|
|
|
393
|
-
-
|
|
437
|
+
- La carga administrativa solo admite imágenes raster JPEG, PNG y WebP. SVG,
|
|
438
|
+
animaciones, video y documentos continúan en el backoffice.
|
|
439
|
+
- No elimina ni reemplaza físicamente archivos de la biblioteca.
|
|
394
440
|
- No funciona desde el cliente público del storefront.
|
|
395
441
|
- No acepta GraphQL directo ni credenciales administrativas permanentes.
|
|
396
442
|
- No previsualiza borradores desde el storefront público.
|
package/README.md
CHANGED
|
@@ -34,10 +34,11 @@ import { createContentManagementClient } from "@bacano/sdk/content-management";
|
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
Lee [`CONTENT_TYPE_MANAGEMENT.md`](./CONTENT_TYPE_MANAGEMENT.md) antes de usarlo.
|
|
37
|
-
La guía contiene el flujo de autorización, contratos de campos y
|
|
38
|
-
estados, referencias, cambios destructivos y límites de seguridad.
|
|
39
|
-
|
|
40
|
-
oficial
|
|
37
|
+
La guía contiene el flujo de autorización, contratos de campos, entradas y
|
|
38
|
+
media, estados, referencias, cambios destructivos y límites de seguridad. Con
|
|
39
|
+
el scope `content_media:upload`, un agente puede buscar imágenes existentes y
|
|
40
|
+
cargar JPEG, PNG o WebP mediante el contrato oficial; Bacano valida los bytes,
|
|
41
|
+
normaliza a WebP y registra el resultado en la biblioteca de la empresa.
|
|
41
42
|
|
|
42
43
|
### Auditoría del storefront
|
|
43
44
|
|
|
@@ -113,6 +113,35 @@ export interface ContentManagementEntryList {
|
|
|
113
113
|
entries: ContentManagementEntry[];
|
|
114
114
|
total: number;
|
|
115
115
|
}
|
|
116
|
+
export interface ContentManagementMedia {
|
|
117
|
+
id: string;
|
|
118
|
+
fileId: string;
|
|
119
|
+
name: string;
|
|
120
|
+
mediaKind: "image";
|
|
121
|
+
mimeType: string;
|
|
122
|
+
sizeBytes: number;
|
|
123
|
+
width: number | null;
|
|
124
|
+
height: number | null;
|
|
125
|
+
altText: string;
|
|
126
|
+
createdAt: string;
|
|
127
|
+
}
|
|
128
|
+
export interface ContentManagementMediaListInput {
|
|
129
|
+
search?: string;
|
|
130
|
+
limit?: number;
|
|
131
|
+
offset?: number;
|
|
132
|
+
}
|
|
133
|
+
export interface ContentManagementMediaList {
|
|
134
|
+
items: ContentManagementMedia[];
|
|
135
|
+
total: number;
|
|
136
|
+
}
|
|
137
|
+
export interface ContentManagementImageUploadInput {
|
|
138
|
+
file: Blob | ArrayBuffer | Uint8Array;
|
|
139
|
+
fileName: string;
|
|
140
|
+
altText?: string;
|
|
141
|
+
}
|
|
142
|
+
export interface ContentManagementImageUploadResult extends ContentManagementMedia {
|
|
143
|
+
reused: boolean;
|
|
144
|
+
}
|
|
116
145
|
export interface ContentManagementReferences {
|
|
117
146
|
categories: Array<{
|
|
118
147
|
code: string;
|
|
@@ -245,6 +274,8 @@ export interface ContentManagementClient {
|
|
|
245
274
|
updateEntry(identifier: string, input: ContentManagementEntryInput): Promise<ContentManagementEntry>;
|
|
246
275
|
inspectEntryLifecycle(identifier: string, operation: ContentManagementLifecycleOperation): Promise<ContentManagementLifecycleResult>;
|
|
247
276
|
applyEntryLifecycle(identifier: string, operation: ContentManagementLifecycleOperation): Promise<ContentManagementLifecycleResult>;
|
|
277
|
+
listMedia(input?: ContentManagementMediaListInput): Promise<ContentManagementMediaList>;
|
|
278
|
+
uploadImage(input: ContentManagementImageUploadInput): Promise<ContentManagementImageUploadResult>;
|
|
248
279
|
listGroups(): Promise<ContentManagementGroup[]>;
|
|
249
280
|
getGroup(identifier: string): Promise<ContentManagementGroup>;
|
|
250
281
|
createGroup(input: ContentManagementGroupInput): Promise<ContentManagementGroup>;
|
package/content-management.cjs
CHANGED
|
@@ -68,6 +68,24 @@ function entryListPath(input = {}) {
|
|
|
68
68
|
const query = search.toString();
|
|
69
69
|
return query ? `/entries?${query}` : "/entries";
|
|
70
70
|
}
|
|
71
|
+
function mediaListPath(input = {}) {
|
|
72
|
+
const search = new URLSearchParams();
|
|
73
|
+
if (input.search) search.set("search", input.search);
|
|
74
|
+
if (input.limit !== void 0) search.set("limit", String(input.limit));
|
|
75
|
+
if (input.offset !== void 0) search.set("offset", String(input.offset));
|
|
76
|
+
const query = search.toString();
|
|
77
|
+
return query ? `/media?${query}` : "/media";
|
|
78
|
+
}
|
|
79
|
+
function uploadBody(file) {
|
|
80
|
+
if ("arrayBuffer" in file) return file;
|
|
81
|
+
if (file instanceof ArrayBuffer) return file;
|
|
82
|
+
const bytes = new Uint8Array(file.byteLength);
|
|
83
|
+
bytes.set(file);
|
|
84
|
+
return bytes.buffer;
|
|
85
|
+
}
|
|
86
|
+
function encodedHeader(value) {
|
|
87
|
+
return encodeURIComponent(value.trim());
|
|
88
|
+
}
|
|
71
89
|
function createContentManagementClient(config) {
|
|
72
90
|
const apiUrl = normalizeApiUrl(config.apiUrl);
|
|
73
91
|
const fetchImplementation = config.fetch ?? globalThis.fetch;
|
|
@@ -98,7 +116,7 @@ function createContentManagementClient(config) {
|
|
|
98
116
|
headers: {
|
|
99
117
|
Accept: "application/json",
|
|
100
118
|
Authorization: `Bearer ${await getAccessToken()}`,
|
|
101
|
-
...init?.body ? { "Content-Type": "application/json" } : {},
|
|
119
|
+
...typeof init?.body === "string" ? { "Content-Type": "application/json" } : {},
|
|
102
120
|
...init?.headers
|
|
103
121
|
}
|
|
104
122
|
}
|
|
@@ -161,6 +179,26 @@ function createContentManagementClient(config) {
|
|
|
161
179
|
),
|
|
162
180
|
inspectEntryLifecycle: (identifier, operation) => lifecycle("entries", identifier, operation, true),
|
|
163
181
|
applyEntryLifecycle: (identifier, operation) => lifecycle("entries", identifier, operation, false),
|
|
182
|
+
listMedia: (input) => request(mediaListPath(input)),
|
|
183
|
+
uploadImage: (input) => {
|
|
184
|
+
const fileName = input.fileName.trim();
|
|
185
|
+
if (!fileName) {
|
|
186
|
+
throw new ContentManagementError({
|
|
187
|
+
message: "A descriptive image file name is required.",
|
|
188
|
+
code: "CONTENT_MEDIA_FILE_NAME_REQUIRED",
|
|
189
|
+
status: 0
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return request("/media/images", {
|
|
193
|
+
method: "POST",
|
|
194
|
+
headers: {
|
|
195
|
+
"Content-Type": "application/octet-stream",
|
|
196
|
+
"X-Bacano-File-Name": encodedHeader(fileName),
|
|
197
|
+
...input.altText ? { "X-Bacano-Alt-Text": encodedHeader(input.altText) } : {}
|
|
198
|
+
},
|
|
199
|
+
body: uploadBody(input.file)
|
|
200
|
+
});
|
|
201
|
+
},
|
|
164
202
|
listGroups: () => request("/groups"),
|
|
165
203
|
getGroup: (identifier) => request(
|
|
166
204
|
`/groups/${encodeIdentifier(identifier)}`
|
package/content-management.js
CHANGED
|
@@ -43,6 +43,24 @@ function entryListPath(input = {}) {
|
|
|
43
43
|
const query = search.toString();
|
|
44
44
|
return query ? `/entries?${query}` : "/entries";
|
|
45
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
|
+
}
|
|
46
64
|
function createContentManagementClient(config) {
|
|
47
65
|
const apiUrl = normalizeApiUrl(config.apiUrl);
|
|
48
66
|
const fetchImplementation = config.fetch ?? globalThis.fetch;
|
|
@@ -73,7 +91,7 @@ function createContentManagementClient(config) {
|
|
|
73
91
|
headers: {
|
|
74
92
|
Accept: "application/json",
|
|
75
93
|
Authorization: `Bearer ${await getAccessToken()}`,
|
|
76
|
-
...init?.body ? { "Content-Type": "application/json" } : {},
|
|
94
|
+
...typeof init?.body === "string" ? { "Content-Type": "application/json" } : {},
|
|
77
95
|
...init?.headers
|
|
78
96
|
}
|
|
79
97
|
}
|
|
@@ -136,6 +154,26 @@ function createContentManagementClient(config) {
|
|
|
136
154
|
),
|
|
137
155
|
inspectEntryLifecycle: (identifier, operation) => lifecycle("entries", identifier, operation, true),
|
|
138
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
|
+
},
|
|
139
177
|
listGroups: () => request("/groups"),
|
|
140
178
|
getGroup: (identifier) => request(
|
|
141
179
|
`/groups/${encodeIdentifier(identifier)}`
|
package/package.json
CHANGED
|
@@ -77,7 +77,8 @@ pnpm exec bacano-figma-web validate-snapshot .bacano/designs/<proyecto>/manifest
|
|
|
77
77
|
## 4. Mapear a Bacano antes de programar
|
|
78
78
|
|
|
79
79
|
Leer [references/bacano-mapping.md](references/bacano-mapping.md),
|
|
80
|
-
[references/content-workflow.md](references/content-workflow.md)
|
|
80
|
+
[references/content-workflow.md](references/content-workflow.md),
|
|
81
|
+
[references/content-media.md](references/content-media.md) y
|
|
81
82
|
[references/implementation-quality.md](references/implementation-quality.md).
|
|
82
83
|
Preparar una matriz ruta → frame → componente → fuente de datos → Content →
|
|
83
84
|
responsive → preview → estado, y determinar si el trabajo comienza desde
|
|
@@ -103,7 +104,10 @@ blueprint, leer la sección de automatización de
|
|
|
103
104
|
componentes, grupos, listas y entradas únicamente mediante el acceso temporal
|
|
104
105
|
y los scopes oficiales. Inventariar esos recursos antes de crear otros y
|
|
105
106
|
preparar entradas en borrador por defecto; la media permanece en el
|
|
106
|
-
backoffice
|
|
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.
|
|
107
111
|
|
|
108
112
|
Al proponer keys nuevas, resolver primero el idioma según
|
|
109
113
|
`CONTENT_TYPE_MANAGEMENT.md`: instrucción explícita, convención existente y
|
|
@@ -129,7 +133,9 @@ lo pendiente cuando la revisión visual quede a cargo del usuario.
|
|
|
129
133
|
|
|
130
134
|
No añadir paquetes, fuentes, iconos o assets si el proyecto ya ofrece un
|
|
131
135
|
equivalente. Descargar assets mediante la fuente Figma autorizada; no incrustar
|
|
132
|
-
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.
|
|
133
139
|
|
|
134
140
|
## 6. Validar diseño y contrato
|
|
135
141
|
|
|
@@ -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.
|
|
@@ -155,7 +155,9 @@ usuario autoriza que el agente materialice el blueprint, usar exclusivamente
|
|
|
155
155
|
agente**. Confirmar la empresa y los scopes, inventariar primero, crear
|
|
156
156
|
componentes antes de sus consumidores y trabajar inicialmente en borrador. La
|
|
157
157
|
skill puede preparar entradas cuando el usuario conceda
|
|
158
|
-
`content_entries:manage
|
|
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.
|
|
159
161
|
|
|
160
162
|
## 6. Cambio a Bacano real
|
|
161
163
|
|
|
@@ -189,7 +191,8 @@ escribir:
|
|
|
189
191
|
locale; crear las faltantes en borrador y actualizar únicamente con
|
|
190
192
|
`expectedUpdatedAt`.
|
|
191
193
|
7. No fusionar por nombres, no usar HTML crudo y no relacionar URLs remotas. La
|
|
192
|
-
media debe existir en Bacano y pertenecer a la empresa autorizada
|
|
194
|
+
media debe existir en Bacano y pertenecer a la empresa autorizada; resolver
|
|
195
|
+
su `fileId` mediante `listMedia()` o `uploadImage()`.
|
|
193
196
|
8. Para archivar o eliminar tipos, entradas, grupos o listas, inspeccionar
|
|
194
197
|
dependencias y confirmar nuevamente. Una actualización de lista debe enviar
|
|
195
198
|
todos los elementos que deben permanecer, incluidos sus IDs existentes.
|
|
@@ -198,5 +201,5 @@ escribir:
|
|
|
198
201
|
|
|
199
202
|
Nunca exponer el token en el navegador, commit, blueprint, salida de consola o
|
|
200
203
|
chat. Nunca convertir el blueprint en una importación implícita ni usar GraphQL
|
|
201
|
-
directo.
|
|
202
|
-
|
|
204
|
+
directo. Sin `content_media:upload`, toda media permanece bajo gestión
|
|
205
|
+
editorial en el backoffice.
|