Medien

Bilder und Dateien liegen in der Medienbibliothek des Workspace. Wie Blöcke sie referenzieren, warum jeder Upload seine eigene URL bekommt und was das beim Ersetzen bedeutet.

Medien liegen auf Workspace-Ebene, nicht auf Seitenebene. Eine Bibliothek, in Ordnern organisiert, geteilt von jeder Seite und jedem Block.

Was du hochladen kannst

Ein Größenlimit und eine Allowlist, beide serverseitig erzwungen und beide in einer einzigen Datei, damit die Vorprüfung im Admin und die API nicht auseinanderlaufen können:

  • So viel, wie dein Tarif erlaubt - 50 MB bei Free, 200 MB bei Starter, 500 MB bei Pro, 2047 MB bei Enterprise, und bei einer ausgehandelten Vereinbarung, was dort steht. Darüber nennt die Ablehnung deine eigene Zahl: File too large (max 500MB on the pro plan). Das Limit gilt je Datei, nicht je Stapel.
  • Jedes Bild, Video und Audio - das gesamte image/*, video/* und audio/* - dazu PDF, Word und Excel nach exaktem Typ. Alles andere wird abgelehnt.

Der Speicher gehört der Organisation, nicht dem Workspace. Das Kontingent des Tarifs wird über alle Workspaces der Organisation geteilt, und ein Upload, der die Organisation darüber hinaustragen würde, wird abgelehnt, bevor ein Byte fließt:

Storage limit exceeded. Current: 812.4MB, Limit: 1024MB

Das Kontingent darf exakt gefüllt werden - es scheitert der Upload, der die Linie überschreiten würde, nicht der, der genau auf ihr landet.

Hochladen aus eigenem Code

Bytes fließen nie durch cmssy. media.authorizeUpload prüft zuerst Typ, Größe und Restkontingent und gibt dann einen presigned PUT zurück, eine Stunde gültig; die Datei geht direkt in den Speicher, und die Bibliothek verzeichnet das Asset danach. Das Upload-Tool des MCP-Servers ist genau dieser Weg, nur verpackt - deshalb unterliegt ein geskripteter Upload denselben drei Prüfungen wie ein Upload von Hand.

Tags

Jedes Asset trägt Tags - einzeln bearbeitbar oder auf eine Auswahl in einem Rutsch angewendet - und die Bibliothek lässt sich auf einen davon einschränken. Eine Datei liegt in genau einem Ordner, kann aber beliebig viele Tags tragen - und das rettet dich, wenn der Ordnerbaum nicht mehr dazu passt, wie Menschen tatsächlich suchen.

Das Medienfeld

Ein Block erreicht ein Asset über fields.media:

export const imageProps = {
  src: fields.media({ label: "Image", required: true }),
  alt: fields.text({ label: "Alt text" }),
};

Du schreibst eine Referenz und liest ein aufgelöstes Objekt. Das ist nicht dieselbe Form, und genau daran scheitern viele:

interface MediaReference {   // was gespeichert wird
  assetId: string;
}

interface ResolvedMedia {    // was deine Komponente bekommt
  id: string;
  url: string | null;
  visibility: "public" | "private";
  alt?: string;
  width?: number;
  height?: number;
}

Lies über die Helfer statt über das Feld. Sie akzeptieren das aufgelöste Objekt ebenso wie den älteren reinen String, sodass dieselbe Komponente auch gegen ein älteres cmssy funktioniert:

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 ist string | null, und dieses null ist kein Fehlerzustand: ein privates Asset löst für eine Leserin ohne Berechtigung zu null auf. Rendere bedingt, dann degradiert die Seite zu keinem Bild statt zu einem kaputten.

width und height kommen zurück, wenn das Asset sie hat - genau das, was next/image für sein Layout ohne zusätzlichen Roundtrip braucht.

Jeder Upload bekommt seine eigene URL

Hochgeladene Assets werden von einem CDN-Host ausgeliefert, mit einem Hash im Pfad:

https://assets.cmssy.io/{workspaceId}/78aa0167-cmssy-og-default.png

Dieser Hash gilt pro Upload. Genau das macht Assets unveränderlich und dauerhaft cachebar - hat aber eine Folge, die man meist auf die harte Tour lernt:

Einen Ersatz hochzuladen aktualisiert die Blöcke nicht, die auf die alte Datei zeigen. Ein neuer Upload ist eine neue URL; bestehende Blöcke behalten die alte und rendern weiter das alte Bild. Wenn du ein Logo getauscht hast und die Site zeigt weiter das vorherige, ist nichts falsch gecacht - die Blöcke zeigen schlicht dorthin, wohin sie immer zeigten.

Ein Ersetzen an Ort und Stelle gibt es nicht: Ein Upload ist immer eine neue Datei und damit eine neue URL. Das Mittel ist, die Blöcke neu zuzuweisen: ein Suchen-und-Ersetzen über Blockinhalte, worin der MCP-Server gut ist.

Die praktische Folge lohnt sich einzuplanen. Ein Asset, auf das viele Seiten verweisen - ein Logo, ein Standard-OG-Bild - lässt sich billiger tauschen, wenn es über die Site-Config oder einen einzigen Block läuft, statt in zwanzig Stellen kopiert zu sein.

Bildtransformationen

Eine Medienreferenz kann eine Transformation tragen; die Auslieferung löst sie zu einer skalierten URL statt zum Original auf:

{
  "assetId": "6a6495e44d1ee7dedcae1f52",
  "transform": { "width": 800, "fit": "cover", "quality": 85 }
}

Deine Komponente bekommt eine Bildskalierungs-URL, gebaut aus der kanonischen:

https://assets.cmssy.io/cdn-cgi/image/format=auto,width=800,fit=cover,quality=85,gravity=auto/{workspaceId}/78aa0167-hero.jpg

format=auto ist immer gesetzt, ein Browser mit AVIF- oder WebP-Unterstützung bekommt es also, ohne dass du danach fragst.

  • width und height rasten nach oben auf die nächste der Größen 96, 200, 400, 800, 1200, 1600, 2400 ein - 2400 ist die Obergrenze. Fragst du 810 an, bekommst du 1200. Die Leiter ist Absicht: eine feste Menge Breiten ist ein Cache, der sich füllt, eine offene ist ein Cache, der nie trifft.
  • fit ist eines von scale-down, contain, cover, crop, pad. Ohne Angabe ist es cover, wenn du beide Maße gegeben hast, und scale-down, wenn du eines gegeben hast.
  • quality ist 1-100, ohne Angabe 85.

Eine Transformation außerhalb dieser Grenzen wird beim Schreiben abgelehnt - Media transform must be within the sizes and quality this workspace can serve - statt still verworfen zu werden. Ein Tippfehler zeigt sich also beim Speichern und nicht Monate später auf einer Seite, die niemand öffnet.

Fokuspunkt

Ein Zuschnitt nutzt gravity=auto, solange das Asset keinen Fokuspunkt trägt. Setze ihn einmal an der Datei in der Medienbibliothek, und jeder Zuschnitt respektiert ihn:

.../cdn-cgi/image/format=auto,width=400,height=400,fit=cover,quality=85,gravity=0.5x0.33/...

Er sitzt am Asset und nicht an der Referenz, weil er eine Tatsache über das Bild ist - wo das Gesicht ist - und nicht über eine Stelle, an der das Bild auftaucht.

Private Dateien sind die Ausnahme: Sie lösen zu url: null auf, es gibt also nichts zu transformieren. Siehe Private Medien.

Ordner

Ordner sind ein flacher Baum mit parentId. Sie ordnen die Bibliothek für Menschen; sie sind nicht Teil der URL, ein Verschieben bricht also nichts, was darauf zeigt.

Über MCP kannst du auflisten, anlegen, umbenennen, löschen und verschieben - was große Umsortierungen skriptbar macht statt zu einem Nachmittag Drag-and-drop.

Eine Datei löschen

Löschen ist abgesichert. cmssy durchsucht zuerst den ganzen Workspace - Entwurfs- und veröffentlichte Blöcke jeder Seite, Layout-Blöcke und Custom Fields, alle Modell-Datensätze und das Branding in der Site-Config - und verweigert, wenn die Datei irgendwo verwendet wird:

CONFLICT  Cannot delete: hero.jpg is in use.

Bis zu fünf Dateien werden genannt, der Rest wird gezählt. Der Scan trifft über die Asset-Id, ein Verschieben zwischen Ordnern versteckt die Datei also nicht vor dem Wächter.

force setzt die Verweigerung außer Kraft und verlangt ein eigenes Recht, getrennt vom gewöhnlichen Löschen. Eine Datei zu löschen, auf die Seiten zeigen, hinterlässt Löcher in diesen Seiten - dahin gelangt eine Redakteurin nicht aus Versehen.

Aus dem Speicher verschwindet weniger als aus der Bibliothek. Die Bytes gehen nur, wenn kein anderes Asset mehr auf dasselbe gespeicherte Objekt zeigt - sonst verschwindet der Bibliothekseintrag und die Datei bleibt, weil etwas anderes sie weiter ausliefert. Was wirklich frei wird, geht vom Speicherverbrauch des Workspace ab.

Next.js-Bilder

Assets kommen von einem anderen Origin als deine App, next/image lehnt sie also ab, bis der Host erlaubt ist:

// next.config.mjs
const nextConfig = {
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "assets.cmssy.io" },
    ],
  },
};

Ein Wildcard hostname: "**" funktioniert und wird in der Referenz-App genutzt, lässt aber jeden HTTPS-Host durch deinen Bildoptimierer. Den Asset-Host zu nennen ist die engere Wahl und kostet eine Zeile.

Alt-Text

Alt-Text lebt an zwei Stellen, und beide sind echt. Das Asset trägt einen Standard je Sprache, einmal in der Medienbibliothek geschrieben - genau das gibt die Auslieferung in ResolvedMedia.alt zurück, und genau das liest mediaAlt(). Der Block trägt die Überschreibung, denn Alt-Text beschreibt, was ein Bild in diesem Kontext bedeutet: dasselbe Foto braucht in einer Case Study andere Worte als in einer Logo-Wand.

Lies zuerst das Speziellere und falle auf das Allgemeine zurück:

alt={content.alt ?? mediaAlt(content.src) ?? ""}

Gib der Redaktion beides. Der Standard in der Bibliothek sorgt dafür, dass ein Bild nie ganz ohne Alt-Text ausgeliefert wird; das Feld am Block sorgt dafür, dass es dort, wo es zählt, das Richtige sagt.

Nächste Schritte