Media
Obrazy i pliki żyją w bibliotece mediów workspace'u. Jak odwołują się do nich bloki, dlaczego każdy upload ma własny URL i co to znaczy przy podmianie.
Media są na poziomie workspace'u, nie strony. Jedna biblioteka, poukładana w foldery, współdzielona przez każdą stronę i każdy blok.
Co możesz wgrać
Jeden limit rozmiaru i jedna allowlista, obie egzekwowane na serwerze i obie w jednym pliku, żeby pre-check w adminie i API nie mogły się rozjechać:
- Tyle, ile pozwala Twój plan - 50 MB na free, 200 MB na starter, 500 MB na pro, 2047 MB na enterprise, a przy wynegocjowanej umowie tyle, ile ustali. Powyżej odmowa nazywa Twoją własną liczbę:
File too large (max 500MB on the pro plan). Limit dotyczy pliku, nie paczki. - Dowolny obraz, wideo i audio - całe
image/*,video/*iaudio/*- plus PDF, Word i Excel po dokładnym typie. Reszta jest odrzucana.
Storage należy do organizacji, nie do workspace'u. Przydział z planu jest współdzielony przez wszystkie workspace'y w organizacji, a upload, który przekroczyłby pułap, jest odrzucany zanim ruszy jakikolwiek bajt:
Storage limit exceeded. Current: 812.4MB, Limit: 1024MBPrzydział możesz wypełnić co do bajta - pada dopiero ten upload, który przekroczyłby linię, nie ten, który na niej staje.
Wgrywanie z własnego kodu
Bajty nigdy nie przepływają przez cmssy. media.authorizeUpload sprawdza najpierw typ, rozmiar i pozostały przydział, po czym oddaje presigned PUT ważny godzinę; plik wysyłasz prosto do magazynu, a biblioteka zapisuje zasób dopiero potem. Narzędzie uploadu w serwerze MCP jest dokładnie tą ścieżką opakowaną - dlatego skryptowany upload podlega tym samym trzem sprawdzeniom co ręczny.
Tagi
Każdy zasób nosi tagi - edytowalne per plik albo nakładane hurtem na zaznaczenie - a bibliotekę można zawęzić do jednego z nich. Plik leży w dokładnie jednym folderze, ale może nieść dowolnie wiele tagów - i to jest to, co ratuje sytuację, gdy drzewo folderów przestaje odpowiadać temu, jak ludzie naprawdę szukają.
Pole media
Blok sięga po zasób przez fields.media:
export const imageProps = {
src: fields.media({ label: "Image", required: true }),
alt: fields.text({ label: "Alt text" }),
};Zapisujesz referencję, a odczytujesz rozwiązany obiekt. To nie jest ten sam kształt i właśnie na tym ludzie się przewracają:
interface MediaReference { // co trafia do bazy
assetId: string;
}
interface ResolvedMedia { // co dostaje Twój komponent
id: string;
url: string | null;
visibility: "public" | "private";
alt?: string;
width?: number;
height?: number;
}Czytaj przez helpery, nie przez samo pole. Przyjmują zarówno rozwiązany obiekt, jak i starszą wartość stringową, więc ten sam komponent działa też przeciw starszemu cmssy:
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 ma typ string | null, a ten null nie jest stanem błędu: zasób prywatny rozwiązuje się do null dla czytelnika bez uprawnienia. Renderuj warunkowo, a strona zdegraduje się do braku obrazka zamiast do zepsutego.
width i height wracają, gdy zasób je ma - czyli dokładnie to, czego next/image potrzebuje do układu bez dodatkowego zapytania.
Każdy upload dostaje własny URL
Wgrane zasoby są serwowane z hosta CDN, z hashem w ścieżce:
https://assets.cmssy.io/{workspaceId}/78aa0167-cmssy-og-default.pngTen hash jest per upload. To właśnie czyni zasoby niezmiennymi i bezpiecznie cache'owalnymi na zawsze - ale ma konsekwencję, którą ludzie poznają na twardo:
Wgranie zamiennika nie aktualizuje bloków wskazujących na stary plik. Nowy upload to nowy URL; istniejące bloki trzymają stary i dalej renderują stary obrazek. Jeśli podmieniłeś logo, a serwis dalej pokazuje poprzednie, nic nie jest źle zcache'owane - bloki po prostu wskazują tam, gdzie zawsze.
Nie ma podmiany w miejscu: upload to zawsze nowy plik, a więc i nowy URL. Lekarstwem jest przepięcie bloków - search-and-replace po treści bloków, w czym serwer MCP jest akurat dobry.
Praktyczną konsekwencję warto zaplanować z góry. Zasób, do którego odwołuje się wiele stron - logo, domyślny obrazek OG - taniej podmienić, jeśli przechodzi przez konfigurację serwisu albo jeden blok, niż gdy jest wklejony w dwudziestu miejscach.
Transformacje obrazów
Referencja do medium może nieść transformację, a warstwa dostarczania rozwiązuje ją do przeskalowanego URL-a zamiast do oryginału:
{
"assetId": "6a6495e44d1ee7dedcae1f52",
"transform": { "width": 800, "fit": "cover", "quality": 85 }
}Twój komponent dostaje URL przeskalowania obrazu zbudowany z kanonicznego:
https://assets.cmssy.io/cdn-cgi/image/format=auto,width=800,fit=cover,quality=85,gravity=auto/{workspaceId}/78aa0167-hero.jpgformat=auto jest zawsze włączone, więc przeglądarka, która przyjmuje AVIF albo WebP, dostaje je bez proszenia.
widthiheightzaokrąglają się w górę do najbliższej z wartości 96, 200, 400, 800, 1200, 1600, 2400 - i 2400 jest sufitem. Poprosisz o 810, dostaniesz 1200. Ta drabinka jest celowa: stały zbiór szerokości to cache, który się zapełnia, a otwarty to cache, który nigdy nie trafia.fitto jedno zscale-down,contain,cover,crop,pad. Pominięte, jestcover, gdy podałeś oba wymiary, iscale-down, gdy jeden.qualityto 1-100, a gdy pominiesz - 85.
Transformacja poza tymi granicami jest odrzucana przy zapisie - Media transform must be within the sizes and quality this workspace can serve - a nie po cichu pomijana, więc literówka wychodzi przy zapisie, a nie po miesiącach na stronie, której nikt nie otwiera.
Punkt centralny
Kadrowanie używa gravity=auto, dopóki zasób nie ma punktu centralnego. Ustaw go raz na pliku w bibliotece mediów, a każde jego kadrowanie będzie go respektować:
.../cdn-cgi/image/format=auto,width=400,height=400,fit=cover,quality=85,gravity=0.5x0.33/...Siedzi na zasobie, a nie na referencji, bo to fakt o zdjęciu - gdzie jest twarz - a nie o jednym miejscu, w którym zdjęcie występuje.
Wyjątkiem są pliki prywatne: rozwiązują się do url: null, więc nie ma czego transformować. Zobacz Media prywatne.
Foldery
Foldery to płaskawe drzewo z parentId. Porządkują bibliotekę dla ludzi; nie są częścią URL-a, więc przeniesienie zasobu między folderami niczego nie psuje.
Przez MCP możesz listować, tworzyć, zmieniać nazwy, usuwać i przenosić - co zamienia hurtową reorganizację w skrypt zamiast popołudnia przeciągania myszą.
Usuwanie pliku
Usuwanie jest pilnowane. cmssy najpierw skanuje cały workspace - bloki robocze i opublikowane każdej strony, bloki layoutu i pola własne, wszystkie rekordy modeli oraz branding w konfiguracji serwisu - i odmawia, jeśli plik jest gdziekolwiek używany:
CONFLICT Cannot delete: hero.jpg is in use.Nazwanych jest do pięciu plików; resztę komunikat zlicza. Skan dopasowuje po id zasobu, więc przeniesienie pliku między folderami nie ukryje go przed strażnikiem.
force przepycha odmowę i wymaga własnego uprawnienia, odrębnego od zwykłego usuwania. Skasowanie pliku, na który wskazują strony, zostawia w nich dziury, więc redaktor nie trafia tam przypadkiem.
Ze storage znika mniej niż z biblioteki. Bajty lecą tylko wtedy, gdy żaden inny zasób nie wskazuje na ten sam obiekt w magazynie - w przeciwnym razie wpis w bibliotece znika, a plik zostaje, bo coś innego wciąż go serwuje. To, co naprawdę się zwolniło, schodzi z zużycia storage workspace'u.
Obrazy w Next.js
Zasoby przychodzą z innego originu niż Twoja aplikacja, więc next/image będzie je odrzucać, dopóki host nie zostanie dopuszczony:
// next.config.mjs
const nextConfig = {
images: {
remotePatterns: [
{ protocol: "https", hostname: "assets.cmssy.io" },
],
},
};Wildcard hostname: "**" działa i tego używa aplikacja referencyjna, ale przepuszcza dowolny host HTTPS przez Twój optymalizator obrazów. Nazwanie hosta zasobów jest cięższym wyborem i kosztuje jedną linię.
Alt
Alt żyje w dwóch miejscach i oba są prawdziwe. Zasób niesie wartość domyślną per język, wpisaną raz w bibliotece mediów - to właśnie zwraca dostarczanie w ResolvedMedia.alt i to czyta mediaAlt(). Blok niesie nadpisanie, bo alt opisuje, co obrazek znaczy w tym kontekście: to samo zdjęcie potrzebuje innych słów w case study i w ścianie logotypów.
Czytaj najpierw to, co bardziej szczegółowe, i schodź do ogólnego:
alt={content.alt ?? mediaAlt(content.src) ?? ""}Daj redaktorom jedno i drugie. Domyślny alt w bibliotece sprawia, że obrazek nigdy nie wyjedzie zupełnie bez altu; pole na bloku sprawia, że tam, gdzie to ważne, powie właściwą rzecz.
Następne kroki
- Media prywatne - widoczność, tokeny assetów i podpisane URL-e.
- Schemat bloku i typy pól -
fields.mediapośród reszty. - Serwer MCP - skryptowanie uploadów i przenoszenia.
- Branding - logo i obrazek OG w konfiguracji serwisu.