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/* i audio/* - 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: 1024MB

Przydział 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.png

Ten 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.jpg

format=auto jest zawsze włączone, więc przeglądarka, która przyjmuje AVIF albo WebP, dostaje je bez proszenia.

  • width i height zaokrą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.
  • fit to jedno z scale-down, contain, cover, crop, pad. Pominięte, jest cover, gdy podałeś oba wymiary, i scale-down, gdy jeden.
  • quality to 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