Sanity, czyli treść przechowywana jak dane
Większość systemów zarządzania treścią myśli stronami: masz stronę, ona ma tytuł, treść i obrazek. Sanity myśli dokumentami o dowolnej strukturze, które da się zapytać, powiązać ze sobą i wykorzystać w wielu miejscach naraz.
Ta różnica brzmi teoretycznie i ma bardzo praktyczne konsekwencje. Opis produktu przechowywany jako dane trafia na stronę, do aplikacji mobilnej, do pliku dla porównywarki i do wiadomości wysyłanej klientowi, za każdym razem w innej formie, bez kopiowania czegokolwiek.
Panel redakcyjny jest tu aplikacją, którą uruchamiasz u siebie i konfigurujesz kodem. Treść leży w usłudze zarządzanej przez producenta, a Ty łączysz się z nią z dowolnego miejsca.
Schemat opisany w kodzie
Strukturę treści definiujesz w plikach, a nie klikaniem w interfejsie. To odróżnia ten system od większości konkurencji i ma zwykłe zalety pracy z kodem: przegląd zmian, historię i możliwość odtworzenia całości w innym środowisku.
import { defineType, defineField } from 'sanity'
export const artykul = defineType({
name: 'artykul',
title: 'Artykuł',
type: 'document',
fields: [
defineField({ name: 'tytul', type: 'string', validation: (r) => r.required() }),
defineField({ name: 'slug', type: 'slug', options: { source: 'tytul' } }),
defineField({ name: 'tresc', type: 'array', of: [{ type: 'block' }] }),
defineField({ name: 'autor', type: 'reference', to: [{ type: 'osoba' }] }),
defineField({ name: 'opublikowany', type: 'datetime' }),
],
})Panel redakcyjny powstaje z tego opisu automatycznie i można go dowolnie zmieniać: dokładać własne widoki, zmieniać układ pól, dopisywać podgląd i akcje. To rzadkość, bo w większości systemów panel jest tym, czym jest.
Zaleta ma cenę i warto ją nazwać wprost. Zmiana struktury treści wymaga edycji kodu i wdrożenia panelu, więc redakcja nie doda sobie nowego pola sama. Przy zespole bez wsparcia technicznego to bywa realnym problemem, a przy zespole z programistami raczej zaletą, bo model treści nie rozjeżdża się po pół roku klikania w interfejsie.
Własny język zapytań
Zamiast pobierać cały dokument i przetwarzać go w aplikacji, opisujesz dokładnie to, czego potrzebujesz, i dostajesz gotowy kształt.
*[_type == "artykul" && opublikowany < now()] | order(opublikowany desc) [0...10] {
tytul,
"slug": slug.current,
"autor": autor->imie,
"liczbaKomentarzy": count(*[_type == "komentarz" && artykul._ref == ^._id])
}To jedno zapytanie robi cztery rzeczy: filtruje po typie i dacie, sortuje, ogranicza liczbę wyników i rozwija powiązanie z autorem, a przy okazji liczy komentarze podzapytaniem. Wynik przychodzi w dokładnie takim kształcie, jaki opisałeś, więc po stronie aplikacji nie ma żadnego przetwarzania.
Zmiana kształtu danych po stronie zapytania zamiast w kodzie ma konkretną wartość przy wydajności. Zamiast pobierać pięć pól i używać jednego, pobierasz jedno.
Po stronie kodu wywołanie wygląda zwyczajnie, a dwie rzeczy w konfiguracji klienta decydują o tym, czy strona działa szybko i czy pokazuje właściwą wersję treści.
import { createClient } from '@sanity/client'
export const sanity = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: 'production',
apiVersion: '2026-08-01',
useCdn: true
})
const artykuly = await sanity.fetch<Artykul[]>(ZAPYTANIE, { limit: 10 })Wersja API podana jako data zamraża zachowanie zapytań, więc zmiana po stronie dostawcy nie zmieni wyniku Twojego kodu bez Twojej decyzji. Sieć dostarczania treści warto włączyć wszędzie poza podglądem wersji roboczych, bo tam potrzebujesz danych świeżych, a nie szybkich.
Składnia jest nietypowa i przez pierwszy dzień bywa frustrująca, zwłaszcza przy strzałkach rozwijających powiązania i przy odwołaniach do dokumentu nadrzędnego w podzapytaniach.
Wartości zmienne przekazuj jako parametry, a nie przez sklejanie łańcuchów znaków. To nie jest wyłącznie kwestia czytelności.
const ZAPYTANIE = `*[_type == "artykul" && kategoria->slug.current == $kategoria]
| order(opublikowany desc) [0...$limit] { tytul, "slug": slug.current }`
const wynik = await sanity.fetch(ZAPYTANIE, {
kategoria: paramsZAdresu.kategoria,
limit: 10
})Wstawienie wartości pochodzącej od użytkownika wprost do treści zapytania pozwala jej zmienić sens całego wyrażenia, dokładnie tak jak przy zapytaniach do bazy relacyjnej. Parametry rozwiązują to raz i przy okazji pozwalają buforować samo zapytanie. Po pierwszym dniu składnia okazuje się wygodniejsza niż alternatywy, bo pozwala napisać w jednym zapytaniu to, co gdzie indziej wymaga trzech.
Dostępny jest również interfejs w powszechniej znanym języku zapytań, przy czym jest wtórny wobec natywnego i nie obejmuje wszystkiego, co ten potrafi.
Tekst sformatowany jako dane
Treść redakcyjna nie jest tu przechowywana jako kod strony, tylko jako struktura opisująca akapity, style i osadzone elementy. To decyzja, która na początku wygląda na komplikację, a rozwiązuje kilka realnych problemów.
Ten sam tekst wyświetlisz na stronie, w aplikacji mobilnej i w wiadomości pocztowej, za każdym razem renderując go inaczej. Nie ma ryzyka, że kod wklejony przez redakcję zepsuje układ albo wpuści coś niebezpiecznego, bo to nie jest kod strony.
Osadzone elementy mogą być dowolnymi obiektami. Blok z kodem, galeria, wyróżniony cytat, karta produktu: każde z nich jest wpisem o określonym typie, który renderujesz własnym komponentem.
Ceną jest konieczność napisania warstwy renderującej. Gotowe biblioteki pokrywają większość przypadków, natomiast każdy własny typ osadzonego elementu wymaga własnego komponentu.
import { PortableText } from '@portabletext/react'
const komponenty = {
types: {
blokKodu: ({ value }) => (
<pre><code className={value.jezyk}>{value.kod}</code></pre>
),
obraz: ({ value }) => (
<img src={urlDlaObrazu(value).width(800).url()} alt={value.alt ?? ''} />
)
},
marks: {
link: ({ value, children }) => (
<a href={value.href} rel="noreferrer">{children}</a>
)
}
}
<PortableText value={artykul.tresc} components={komponenty} />Warto o tym pamiętać, projektując model treści. Piętnaście rodzajów bloków oznacza piętnaście komponentów do napisania i utrzymania, a redakcja zwykle używa czterech.
Ceny i pułapki
Darmowy próg jest tu szeroki i obejmuje kilka osób w zespole, dziesiątki tysięcy dokumentów oraz jeden zbiór danych. Dla bloga, portfolio i małego serwisu to wystarcza na długo.
Powyżej progu plany różnią się liczbą osób, liczbą zbiorów danych i limitami ruchu. Konkretne stawki różnią się między źródłami i zmieniają w czasie, więc przed policzeniem budżetu sprawdź aktualny cennik u producenta.
Trzy rzeczy potrafią zaskoczyć przy rachunku i warto znać je z góry.
Pierwsza to ruch przez sieć dostarczania treści. Obrazy serwowane z ich usługi liczą się do limitu, a przy serwisie z dużą liczbą zdjęć ta pozycja rośnie szybciej niż liczba dokumentów.
Druga to zapytania. Aplikacja odpytująca usługę przy każdym żądaniu użytkownika zużywa znacznie więcej niż aplikacja generująca strony statycznie i odświeżająca je co godzinę. Przy pracy z Next.js warto skorzystać z buforowania i regeneracji przyrostowej, zamiast pytać przy każdym wejściu.
Trzecia to zbiory danych. Osobny zbiór na środowisko testowe wygląda niewinnie i bywa liczony jako kolejna pozycja w planie, więc przy trzech środowiskach rachunek rośnie zauważalnie.
Praca na żywo i wersje robocze
Dwie funkcje, które przy pracy zespołowej decydują o komforcie, a przy prototypie łatwo je przeoczyć.
Panel działa w czasie rzeczywistym, więc dwie osoby edytujące ten sam dokument widzą swoje zmiany na bieżąco, bez blokowania i bez komunikatów o konflikcie. To rozwiązuje najbardziej irytujący problem redakcji: sytuację, w której czyjeś zmiany znikają, bo ktoś inny zapisał starszą wersję.
Wersje robocze działają obok opublikowanych. Dokument ma stan opublikowany widoczny dla czytelników i stan roboczy widoczny wyłącznie w panelu, więc przygotowanie zmian nie wpływa na to, co widzi użytkownik.
Warto to wykorzystać zamiast własnego pola oznaczającego publikację, bo mechanizm wbudowany obsługuje również podgląd. Aplikacja odpytująca usługę z odpowiednim uprawnieniem może pokazać stan roboczy, co daje redakcji podgląd zmian na prawdziwej stronie przed publikacją.
Historia zmian jest zapisywana automatycznie, więc powrót do wcześniejszej wersji dokumentu jest jednym kliknięciem. Zakres przechowywanej historii zależy od planu, co ma znaczenie wtedy, gdy wymogi branżowe nakazują trzymanie zapisu zmian przez określony czas.
Obrazy i pliki
Warstwa mediów jest tu mocniejsza, niż sugeruje opis, i warto skonfigurować ją zanim redakcja wgra pierwsze pięćset zdjęć.
Obrazy przechowywane w usłudze da się przekształcać przez parametry adresu: rozmiar, kadr, format i jakość. To znaczy, że nie generujesz wariantów przy wgrywaniu, tylko prosisz o właściwy rozmiar w momencie wyświetlania, a usługa zwraca go i zapamiętuje.
Wbudowany jest też wybór punktu ostrości. Redakcja wskazuje, co na zdjęciu jest najważniejsze, a kadrowanie do różnych proporcji zachowuje ten punkt, zamiast obcinać zdjęcie po środku. Przy portretach i produktach ta drobna rzecz oszczędza mnóstwo poprawek.
Pole opisu alternatywnego warto ustawić jako wymagane od początku. To jedna z niewielu rzeczy, których nie da się dodać później hurtowo, bo opis musi napisać człowiek patrzący na zdjęcie, a zaległość rośnie szybciej, niż ktokolwiek ją nadrabia.
Zastrzeżenie kosztowe wraca tu ponownie: obrazy serwowane z ich sieci dostarczania treści liczą się do limitu ruchu, więc przy serwisie ze zdjęciami warto rozważyć własną warstwę pośredniczącą albo przynajmniej świadomie dobierać rozmiary, zamiast wysyłać pełne zdjęcia do miniatur.
Sanity kontra alternatywy
| Rozwiązanie | Mocna strona | Słabość | Kiedy wybrać |
|---|---|---|---|
| Sanity | Treść jako dane, własny język zapytań, konfigurowalny panel | Zmiana modelu wymaga programisty | Treść używana w wielu miejscach naraz |
| Payload | Panel w Twojej aplikacji, konfiguracja w kodzie | Wdrożenie po Twojej stronie | Projekt na Next.js z zespołem programistów |
| Strapi | Budowa modelu w panelu, bez programisty | Osobna usługa do utrzymania | Model treści zmieniany przez redakcję |
| Contentful | Dojrzałość, wsparcie dla dużych organizacji | Wysoka cena, sztywniejszy model | Korporacja z wymogami zgodności |
Pierwszy wiersz wygrywa wtedy, gdy ta sama treść trafia w kilka miejsc w różnych formach, bo wtedy przechowywanie jej jako danych zamiast jako gotowej strony zwraca się niemal natychmiast i widać to przy pierwszym nowym kanale.
Przegrywa przy prostym blogu, gdzie treść ma jedno przeznaczenie, a redakcja potrzebuje edytora, który po prostu działa. Wtedy prostsze rozwiązanie oszczędza pracy przy renderowaniu i przy modelu treści.
Wdrożenie panelu i środowiska
Panel jest osobną aplikacją, więc trzeba zdecydować, gdzie i jak działa. To decyzja, którą łatwo odłożyć, a potem trudno cofnąć.
Najprostsza droga to hosting u producenta pod adresem w jego domenie. Nic nie wdrażasz, panel aktualizuje się sam, a redakcja dostaje działający adres tego samego dnia.
Druga droga to umieszczenie panelu we własnej aplikacji, pod ścieżką w Twojej domenie. Wymaga wdrożenia razem z resztą projektu i daje w zamian jeden adres dla wszystkiego oraz możliwość podpięcia własnego uwierzytelniania i własnych reguł dostępu.
Zbiory danych to osobna decyzja i warto ją podjąć od razu. Osobny zbiór na środowisko testowe pozwala eksperymentować bez ryzyka, a jednocześnie liczy się do limitu planu. Rozwiązanie pośrednie polega na pracy na jednym zbiorze produkcyjnym z wykorzystaniem wersji roboczych, co wystarcza przy małym zespole.
Migrację treści między zbiorami obsługuje narzędzie wiersza poleceń, eksportujące i importujące całość razem z plikami. To dobra wiadomość przy zakładaniu środowiska testowego i zła przy przenoszeniu produkcji, bo eksport dużego zbioru to godziny pracy i sporo miejsca na dysku.
Warto też zaplanować kopie zapasowe. Usługa jest zarządzana i ma własne mechanizmy, natomiast eksport wykonywany okresowo i trzymany u siebie chroni przed przypadkowym usunięciem po Twojej stronie, którego żadna kopia dostawcy nie cofnie za Ciebie.
Typowe błędy
Pierwszy to odpytywanie usługi przy każdym żądaniu użytkownika. Buforowanie i generowanie statyczne obniżają zużycie wielokrotnie, a przy treści zmieniającej się kilka razy dziennie nic nie kosztują pod względem świeżości.
Drugi to pobieranie całych dokumentów zamiast wybranych pól. Język zapytań pozwala określić kształt wyniku, więc pobieranie pięciu pól, żeby użyć jednego, jest zbędnym transferem.
Trzeci to zbyt bogaty model treści. Piętnaście rodzajów osadzonych bloków oznacza piętnaście komponentów do napisania i utrzymania, a redakcja i tak używa czterech.
Czwarty to obietnica złożona redakcji, że sama zmieni strukturę treści. Model definiuje się w kodzie, więc każda zmiana wymaga programisty i wdrożenia panelu.
Piąty to pominięcie kosztu ruchu przy obrazach. Przy serwisie ze zdjęciami ta pozycja bywa większa niż wszystko inne razem.
Szósty to trzymanie treści roboczej w tym samym zbiorze co produkcyjna bez rozróżnienia stanu. Mechanizm wersji roboczych istnieje, obsługuje też podgląd na prawdziwej stronie i sprawdza się lepiej niż dodatkowe pole oznaczające publikację.
Siódmy to brak własnego eksportu treści. Usługa jest zarządzana i ma swoje mechanizmy, natomiast przypadkowe usunięcie po Twojej stronie cofniesz wyłącznie własną kopią.
FAQ
Czy Sanity jest darmowe?
Do pewnego poziomu tak: darmowy próg obejmuje kilka osób w zespole, dziesiątki tysięcy dokumentów oraz jeden zbiór danych, co przy blogu i małym serwisie wystarcza na długo. Powyżej progu plany różnią się liczbą osób i limitami ruchu, a stawki warto sprawdzić w aktualnym cenniku.
Czym jest ten własny język zapytań?
Sposobem opisywania dokładnie tego, czego potrzebujesz, razem z filtrowaniem, sortowaniem, rozwijaniem powiązań i podzapytaniami w jednym wywołaniu. Wynik przychodzi w kształcie, który podałeś, więc po stronie aplikacji nie ma przetwarzania danych.
Dlaczego treść nie jest przechowywana jako kod strony?
Bo ta sama treść ma trafiać w wiele miejsc w różnych formach: na stronę, do aplikacji i do wiadomości. Struktura opisująca akapity i osadzone elementy pozwala renderować ją inaczej w każdym z nich, kosztem napisania warstwy renderującej.
Czy redakcja poradzi sobie bez programisty?
W codziennej pracy tak, bo panel jest kompletny i przyjazny. Zmiana struktury treści wymaga natomiast edycji kodu i wdrożenia, więc przy zespole bez wsparcia technicznego lepszy bywa Strapi albo inne rozwiązanie z modelem budowanym w interfejsie.
Jak ograniczyć koszty?
Trzema rzeczami: generowaniem stron statycznie zamiast odpytywania przy każdym żądaniu, pobieraniem wyłącznie potrzebnych pól oraz świadomym decydowaniem o liczbie zbiorów danych i o tym, skąd serwowane są obrazy.
Dokumentacja stoi na stronie projektu, a język zapytań opisuje osobny dział dokumentacji.