Medios
Imágenes y archivos viven en la biblioteca de medios del workspace. Cómo los referencian los bloques, por qué cada subida tiene su propia URL y qué implica al reemplazar.
Los medios son de nivel workspace, no de página. Una biblioteca, organizada en carpetas, compartida por cada página y cada bloque.
Qué puedes subir
Un límite de tamaño y una lista de tipos permitidos, ambos aplicados en el servidor y ambos en un solo archivo, para que la comprobación previa del admin y la API no puedan desviarse:
- Lo que permita tu plan: 50 MB en Free, 200 MB en Starter, 500 MB en Pro, 2047 MB en Enterprise, y lo que fije un acuerdo negociado. Por encima, el rechazo nombra tu propia cifra:
File too large (max 500MB on the pro plan). El límite es por archivo, no por lote. - Cualquier imagen, vídeo o audio -todo
image/*,video/*yaudio/*- más PDF, Word y Excel por tipo exacto. Lo demás se rechaza.
El almacenamiento pertenece a la organización, no al workspace. La asignación del plan se comparte entre todos los workspaces de la organización, y una subida que la sobrepasaría se rechaza antes de que se mueva un solo byte:
Storage limit exceeded. Current: 812.4MB, Limit: 1024MBLa asignación puede llenarse exactamente: falla la subida que cruzaría la línea, no la que aterriza sobre ella.
Subir desde tu propio código
Los bytes nunca pasan por cmssy. media.authorizeUpload comprueba primero el tipo, el tamaño y la asignación restante, y devuelve un PUT prefirmado válido una hora; el archivo va directo al almacenamiento y la biblioteca registra el recurso después. La herramienta de subida del servidor MCP es exactamente ese camino envuelto, y por eso una subida automatizada pasa por las mismas tres comprobaciones que una manual.
Etiquetas
Cada recurso lleva etiquetas -editables archivo a archivo o aplicadas de golpe a una selección- y la biblioteca puede filtrarse por una de ellas. Un archivo vive en exactamente una carpeta, pero puede llevar tantas etiquetas como necesite, que es lo que te salva cuando el árbol de carpetas deja de coincidir con cómo busca la gente.
El campo de medios
Un bloque alcanza un recurso mediante fields.media:
export const imageProps = {
src: fields.media({ label: "Image", required: true }),
alt: fields.text({ label: "Alt text" }),
};Escribes una referencia y lees un objeto resuelto. No son la misma forma, y ahí es donde la gente tropieza:
interface MediaReference { // lo que se guarda
assetId: string;
}
interface ResolvedMedia { // lo que recibe tu componente
id: string;
url: string | null;
visibility: "public" | "private";
alt?: string;
width?: number;
height?: number;
}Lee a través de los helpers en vez del campo. Aceptan tanto el objeto resuelto como el valor antiguo en cadena, así que el mismo componente funciona también contra un cmssy más antiguo:
import { mediaUrl, mediaAlt } from "@cmssy/react";
function ImageBlock({ content }) {
const src = mediaUrl(content.src);
if (!src) return null;
return <img src={src} alt={content.alt ?? mediaAlt(content.src) ?? ""} />;
}url es string | null, y ese null no es un estado de error: un recurso privado se resuelve a null para quien no tiene derecho de acceso. Renderiza condicionalmente y la página degrada a ninguna imagen en lugar de a una rota.
width y height vuelven cuando el recurso los tiene, que es justo lo que next/image necesita para su maquetación sin una ida y vuelta extra.
Cada subida obtiene su propia URL
Los recursos subidos se sirven desde un host CDN, con un hash en la ruta:
https://assets.cmssy.io/{workspaceId}/78aa0167-cmssy-og-default.pngEse hash es por subida. Es lo que hace los recursos inmutables y cacheables para siempre, pero tiene una consecuencia que se suele aprender por las malas:
Subir un reemplazo no actualiza los bloques que apuntan al archivo antiguo. Una subida nueva es una URL nueva; los bloques existentes conservan la vieja y siguen mostrando la imagen vieja. Si cambiaste un logo y el sitio sigue mostrando el anterior, no hay nada mal cacheado: los bloques simplemente apuntan donde siempre apuntaron.
No hay sustitución en el sitio: una subida es siempre un archivo nuevo y, por tanto, una URL nueva. El remedio es reapuntar los bloques: un buscar y reemplazar sobre el contenido de bloques, algo en lo que el servidor MCP es bueno.
Conviene planear la consecuencia práctica. Un recurso al que apuntan muchas páginas -un logo, una imagen OG por defecto- sale más barato de cambiar si pasa por la configuración del sitio o por un solo bloque, en vez de estar pegado en veinte lugares.
Transformaciones de imagen
Una referencia de medios puede llevar una transformación, y la entrega la resuelve a una URL redimensionada en lugar del original:
{
"assetId": "6a6495e44d1ee7dedcae1f52",
"transform": { "width": 800, "fit": "cover", "quality": 85 }
}Tu componente recibe una URL de redimensionado construida a partir de la canónica:
https://assets.cmssy.io/cdn-cgi/image/format=auto,width=800,fit=cover,quality=85,gravity=auto/{workspaceId}/78aa0167-hero.jpgformat=auto se aplica siempre, así que un navegador que acepta AVIF o WebP lo recibe sin que lo pidas.
widthyheightse ajustan hacia arriba al valor más cercano entre 96, 200, 400, 800, 1200, 1600, 2400 - y 2400 es el techo. Pide 810 y obtienes 1200. La escalera es deliberada: un conjunto fijo de anchos es una caché que se llena; uno abierto es una caché que nunca acierta.fites uno descale-down,contain,cover,crop,pad. Omitido, escovercuando diste ambas dimensiones yscale-downcuando diste una.qualityva de 1 a 100, y es 85 si lo omites.
Una transformación fuera de esos límites se rechaza al escribirla - Media transform must be within the sizes and quality this workspace can serve - en vez de descartarse en silencio, así que una errata aparece al guardar y no meses después en una página que nadie abre.
Punto focal
Un recorte usa gravity=auto hasta que el recurso lleva un punto focal. Defínelo una vez en el archivo dentro de la biblioteca y cada recorte lo respeta:
.../cdn-cgi/image/format=auto,width=400,height=400,fit=cover,quality=85,gravity=0.5x0.33/...Vive en el recurso y no en la referencia porque es un hecho sobre la imagen -dónde está la cara- y no sobre un lugar donde la imagen aparece.
Los archivos privados son la excepción: se resuelven a url: null, así que no hay nada que transformar. Ver Medios privados.
Carpetas
Las carpetas son un árbol plano con parentId. Organizan la biblioteca para las personas; no forman parte de la URL, así que mover un recurso entre carpetas no rompe nada.
Por MCP puedes listar, crear, renombrar, borrar y mover, lo que convierte una reorganización masiva en un script en vez de una tarde arrastrando.
Borrar un archivo
El borrado está vigilado. cmssy recorre primero todo el workspace -bloques en borrador y publicados de cada página, bloques de layout y campos personalizados, todos los registros de modelos y el branding de la configuración del sitio- y se niega si el archivo se usa en algún sitio:
CONFLICT Cannot delete: hero.jpg is in use.Se nombran hasta cinco archivos; el resto lo cuenta el mensaje. El barrido casa por id del recurso, así que mover un archivo entre carpetas no lo esconde del guardián.
force fuerza el borrado y exige un permiso propio, distinto del borrado ordinario. Borrar un archivo al que apuntan páginas deja agujeros en ellas, así que un editor no llega ahí por accidente.
Del almacenamiento se va menos que de la biblioteca. Los bytes solo desaparecen cuando ningún otro recurso apunta al mismo objeto almacenado; si no, la entrada de la biblioteca se va y el archivo se queda, porque algo más lo sigue sirviendo. Lo que realmente se libera se descuenta del almacenamiento usado por el workspace.
Imágenes en Next.js
Los recursos vienen de un origen distinto al de tu app, así que next/image los rechazará hasta que permitas el host:
// next.config.mjs
const nextConfig = {
images: {
remotePatterns: [
{ protocol: "https", hostname: "assets.cmssy.io" },
],
},
};Un comodín hostname: "**" funciona y es lo que usa la app de referencia, pero deja pasar cualquier host HTTPS por tu optimizador de imágenes. Nombrar el host de recursos es la opción más estricta y cuesta una línea.
El texto alternativo
El texto alternativo vive en dos sitios, y ambos son reales. El recurso lleva un valor por defecto por idioma, escrito una vez en la biblioteca de medios: eso es lo que la entrega devuelve en ResolvedMedia.alt y lo que lee mediaAlt(). El bloque lleva la sobrescritura, porque el alt describe qué significa una imagen en este contexto: la misma foto necesita otras palabras en un caso de éxito y en un muro de logos.
Lee primero lo específico y cae hacia lo general:
alt={content.alt ?? mediaAlt(content.src) ?? ""}Dale a los editores ambos. El valor por defecto de la biblioteca evita que una imagen salga sin alt alguno; el campo del bloque hace que diga lo correcto donde importa.
Siguientes pasos
- Medios privados: visibilidad, tokens de recurso y URLs firmadas.
- Esquema de bloque y tipos de campo:
fields.mediaentre los demás. - Servidor MCP: automatizar subidas y movimientos.
- Branding: logo e imagen OG en la configuración del sitio.