Médias
Images et fichiers vivent dans la médiathèque de l'espace de travail. Comment les blocs les référencent, pourquoi chaque envoi obtient sa propre URL, et ce que cela implique au remplacement.
Les médias sont au niveau de l'espace de travail, pas de la page. Une bibliothèque, organisée en dossiers, partagée par toutes les pages et tous les blocs.
Ce que vous pouvez envoyer
Une limite de taille et une liste d'autorisation, toutes deux appliquées côté serveur et toutes deux dans un seul fichier, pour que la vérification préalable de l'admin et l'API ne puissent pas diverger :
- Ce que votre formule autorise - 50 Mo en Free, 200 Mo en Starter, 500 Mo en Pro, 2047 Mo en Enterprise, et ce que fixe un accord négocié. Au-delà, le refus nomme votre propre chiffre :
File too large (max 500MB on the pro plan). La limite porte sur le fichier, pas sur le lot. - Toute image, vidéo ou audio - l'intégralité de
image/*,video/*etaudio/*- plus PDF, Word et Excel par type exact. Tout le reste est refusé.
Le stockage appartient à l'organisation, pas à l'espace de travail. Le quota du plan est mutualisé entre tous les espaces de l'organisation, et un envoi qui la ferait dépasser est refusé avant qu'un seul octet ne bouge :
Storage limit exceeded. Current: 812.4MB, Limit: 1024MBLe quota peut être rempli exactement : c'est l'envoi qui franchirait la ligne qui échoue, pas celui qui s'y arrête.
Envoyer depuis votre propre code
Les octets ne transitent jamais par cmssy. media.authorizeUpload vérifie d'abord le type, la taille et le quota restant, puis rend un PUT presigné valable une heure ; vous envoyez le fichier directement au stockage, et la bibliothèque enregistre l'asset ensuite. L'outil d'envoi du serveur MCP n'est que ce chemin emballé - c'est pourquoi un envoi scripté subit les trois mêmes contrôles qu'un envoi manuel.
Étiquettes
Chaque asset porte des étiquettes - modifiables fichier par fichier ou appliquées d'un coup à une sélection - et la bibliothèque peut être filtrée sur l'une d'elles. Un fichier vit dans un seul dossier mais peut porter autant d'étiquettes qu'il lui en faut - ce qui vous sauve quand l'arborescence cesse de correspondre à la façon dont les gens cherchent vraiment.
Le champ média
Un bloc atteint un asset via fields.media :
export const imageProps = {
src: fields.media({ label: "Image", required: true }),
alt: fields.text({ label: "Alt text" }),
};Vous écrivez une référence et vous lisez un objet résolu. Ce n'est pas la même forme, et c'est là que les gens trébuchent :
interface MediaReference { // ce qui est stocké
assetId: string;
}
interface ResolvedMedia { // ce que reçoit votre composant
id: string;
url: string | null;
visibility: "public" | "private";
alt?: string;
width?: number;
height?: number;
}Lisez via les helpers plutôt que via le champ. Ils acceptent aussi bien l'objet résolu que l'ancienne valeur en chaîne, donc le même composant fonctionne aussi face à un cmssy plus ancien :
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 vaut string | null, et ce null n'est pas une erreur : un asset privé se résout à null pour un lecteur sans habilitation. Rendez conditionnellement et la page dégrade vers aucune image plutôt que vers une image cassée.
width et height reviennent quand l'asset les porte - exactement ce dont next/image a besoin pour sa mise en page, sans aller-retour supplémentaire.
Chaque envoi obtient sa propre URL
Les assets envoyés sont servis depuis un hôte CDN, avec un hash dans le chemin :
https://assets.cmssy.io/{workspaceId}/78aa0167-cmssy-og-default.pngCe hash est propre à chaque envoi. C'est ce qui rend les assets immuables et cachables sans limite - mais cela a une conséquence qu'on découvre souvent à la dure :
Téléverser un remplacement ne met pas à jour les blocs qui pointent vers l'ancien fichier. Un nouvel envoi est une nouvelle URL ; les blocs existants gardent l'ancienne et continuent d'afficher l'ancienne image. Si vous avez changé un logo et que le site montre encore le précédent, rien n'est mal mis en cache : les blocs pointent simplement là où ils ont toujours pointé.
Il n'y a pas de remplacement sur place : un envoi est toujours un nouveau fichier, donc une nouvelle URL. Le remède est de re-pointer les blocs - un chercher-remplacer sur le contenu des blocs, ce que le serveur MCP fait bien.
La conséquence pratique mérite d'être anticipée. Un asset référencé par de nombreuses pages - un logo, une image OG par défaut - se remplace à moindre coût s'il passe par la configuration du site ou un bloc unique, plutôt que d'être collé à vingt endroits.
Transformations d'images
Une référence média peut porter une transformation, et la livraison la résout en une URL redimensionnée plutôt qu'en l'original :
{
"assetId": "6a6495e44d1ee7dedcae1f52",
"transform": { "width": 800, "fit": "cover", "quality": 85 }
}Votre composant reçoit une URL de redimensionnement construite à partir de l'URL canonique :
https://assets.cmssy.io/cdn-cgi/image/format=auto,width=800,fit=cover,quality=85,gravity=auto/{workspaceId}/78aa0167-hero.jpgformat=auto est toujours appliqué : un navigateur qui accepte l'AVIF ou le WebP le reçoit sans que vous ayez à le demander.
widthetheightsont arrondis vers le haut à la valeur la plus proche parmi 96, 200, 400, 800, 1200, 1600, 2400 - et 2400 est le plafond. Demandez 810, vous obtenez 1200. L'échelle est délibérée : un jeu fixe de largeurs est un cache qui se remplit, un jeu ouvert est un cache qui ne touche jamais.fitvautscale-down,contain,cover,cropoupad. Omis, c'estcoversi vous avez donné les deux dimensions etscale-downsi vous n'en avez donné qu'une.qualityva de 1 à 100, et vaut 85 par défaut.
Une transformation hors de ces bornes est refusée à l'écriture - Media transform must be within the sizes and quality this workspace can serve - plutôt qu'ignorée en silence : une coquille apparaît au moment où vous enregistrez, et non des mois plus tard sur une page que personne n'ouvre.
Point focal
Un recadrage utilise gravity=auto tant que l'asset ne porte pas de point focal. Définissez-le une fois sur le fichier dans la médiathèque et chaque recadrage le respecte :
.../cdn-cgi/image/format=auto,width=400,height=400,fit=cover,quality=85,gravity=0.5x0.33/...Il vit sur l'asset et non sur la référence, parce que c'est un fait à propos de l'image - où se trouve le visage - et non à propos d'un endroit où elle apparaît.
Les fichiers privés font exception : ils se résolvent à url: null, il n'y a donc rien à transformer. Voir Médias privés.
Dossiers
Les dossiers forment un arbre plat avec un parentId. Ils organisent la bibliothèque pour les humains ; ils ne font pas partie de l'URL, déplacer un asset ne casse donc rien.
Via MCP vous pouvez lister, créer, renommer, supprimer et déplacer - ce qui rend une grande réorganisation scriptable plutôt qu'un après-midi de glisser-déposer.
Supprimer un fichier
La suppression est gardée. cmssy parcourt d'abord tout l'espace de travail - blocs brouillon et publiés de chaque page, blocs de layout et champs personnalisés, tous les enregistrements de modèles, et le branding de la configuration du site - et refuse si le fichier est utilisé quelque part :
CONFLICT Cannot delete: hero.jpg is in use.Jusqu'à cinq fichiers sont nommés ; au-delà, le message compte le reste. Le balayage rapproche par identifiant d'asset : déplacer un fichier entre dossiers ne le cache pas au garde.
force passe outre le refus et exige une permission propre, distincte de la suppression ordinaire. Supprimer un fichier vers lequel des pages pointent laisse des trous dans ces pages : ce n'est pas quelque chose qu'une rédactrice atteint par accident.
Ce qui quitte le stockage est plus étroit que ce qui quitte la bibliothèque. Les octets ne partent que si aucun autre asset ne pointe encore vers le même objet stocké - sinon l'entrée disparaît et le fichier reste, parce que quelque chose d'autre le sert encore. Ce qui est réellement libéré est retiré du stockage consommé par l'espace de travail.
Images Next.js
Les assets viennent d'une autre origine que votre application : next/image les refusera tant que l'hôte n'est pas autorisé :
// next.config.mjs
const nextConfig = {
images: {
remotePatterns: [
{ protocol: "https", hostname: "assets.cmssy.io" },
],
},
};Un joker hostname: "**" fonctionne et c'est ce qu'utilise l'application de référence, mais il laisse passer n'importe quel hôte HTTPS dans votre optimiseur d'images. Nommer l'hôte des assets est le choix plus serré, et coûte une ligne.
Le texte alternatif
Le texte alternatif vit à deux endroits, et les deux sont réels. L'asset porte une valeur par défaut par langue, écrite une fois dans la médiathèque : c'est ce que la livraison renvoie dans ResolvedMedia.alt, et c'est ce que lit mediaAlt(). Le bloc porte la surcharge, car l'alt décrit ce qu'une image signifie dans ce contexte : la même photo demande d'autres mots dans une étude de cas et dans un mur de logos.
Lisez d'abord le plus spécifique et repliez-vous sur le plus général :
alt={content.alt ?? mediaAlt(content.src) ?? ""}Donnez les deux à la rédaction. La valeur par défaut de la bibliothèque évite qu'une image parte sans aucun alt ; le champ du bloc lui permet de dire la bonne chose là où cela compte.
Étapes suivantes
- Médias privés - visibilité, jetons d'asset et URL signées.
- Schéma de bloc & types de champs -
fields.mediaparmi les autres. - Serveur MCP - scripter envois et déplacements.
- Branding - logo et image OG dans la configuration.