Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds
Powrót do kolekcji
Przewodnik11 min czytania

Payload CMS, system treści w kodzie

Payload instaluje się wprost w aplikacji Next.js. Konfiguracja w TypeScripcie, kontrola dostępu, lokalne API, koszty hostingu i sytuacja po przejęciu przez Figmę.

Payload CMS, treść definiowana w kodzie

Systemy zarządzania treścią zwykle stoją obok aplikacji. Uruchamiasz osobną usługę, konfigurujesz ją przez panel, a potem pobierasz dane przez API i użerasz się z zasadami dostępu między domenami.

Payload przewraca ten układ. Od wersji trzeciej instaluje się wprost w aplikacji Next.js, razem z panelem administracyjnym i całym zapleczem. Nie ma osobnej usługi ani warstwy API do utrzymania, a zapytania o treść to zwykłe wywołania z komponentu serwerowego.

Druga różnica jest równie zasadnicza. Struktura treści powstaje w plikach TypeScript, nie w interfejsie graficznym. Kolekcje, pola i reguły dostępu to kod, który leży w repozytorium, przechodzi przegląd i ma historię zmian.

Sytuacja projektu

Zanim przejdziemy do kodu, jedna sprawa, która ma znaczenie przy podejmowaniu decyzji.

Payload został przejęty przez Figmę w czerwcu 2025 roku. Sam projekt pozostał otwarty na licencji pozwalającej na dowolne użycie komercyjne, kod nadal jest publiczny, a rozwój trwa.

Zmieniła się natomiast oferta hostingu. Zapisy do usługi zarządzanej prowadzonej przez twórców zostały wstrzymane po przejęciu i pozostają wstrzymane, a dotychczasowi klienci zachowali dostęp. Praktyczny wniosek jest taki, że nowy projekt trzeba wdrożyć samodzielnie: na własnym serwerze, w kontenerze albo u dostawcy takiego jak Vercel.

To nie jest przeszkoda, bo system był od początku pomyślany jako rozwiązanie do samodzielnego uruchamiania, a hosting był dodatkiem. Warto natomiast wiedzieć o tym przed obiecaniem klientowi gotowej usługi z abonamentem.

Pierwsza konfiguracja

Code
Bash
npx create-payload-app@latest moj-projekt

Kolekcja to opis jednego typu treści. Tak wygląda blog z artykułami i autorami.

Code
TypeScript
import type { CollectionConfig } from 'payload'

export const Artykuly: CollectionConfig = {
  slug: 'artykuly',
  admin: { useAsTitle: 'tytul' },
  access: {
    read: () => true,
    create: ({ req }) => Boolean(req.user),
    update: ({ req }) => Boolean(req.user)
  },
  fields: [
    { name: 'tytul', type: 'text', required: true },
    { name: 'slug', type: 'text', required: true, unique: true },
    { name: 'tresc', type: 'richText' },
    { name: 'autor', type: 'relationship', relationTo: 'uzytkownicy' },
    { name: 'opublikowany', type: 'checkbox', defaultValue: false }
  ]
}

Z tego opisu powstaje kilka rzeczy naraz: tabele w bazie, panel administracyjny do edycji, punkty końcowe API oraz typy TypeScript dla frontendu. Jeden plik zamiast czterech miejsc, które trzeba trzymać w zgodzie.

Typy są tu ważniejsze, niż wygląda. Wygenerowane definicje wchodzą wprost do kodu strony, więc literówka w nazwie pola jest błędem kompilacji, a nie pustym miejscem na stronie odkrytym przez czytelnika.

Kontrola dostępu

To obszar, w którym podejście oparte na kodzie wygrywa najwyraźniej z konfiguracją klikaną w panelu.

Uprawnienia zapisujesz jako funkcje zwracające wartość logiczną albo warunek zapytania. Reguła może zależeć od roli, od właściciela dokumentu, od pory dnia albo od czegokolwiek, co da się wyrazić w kodzie.

Code
TypeScript
access: {
  read: ({ req }) => {
    if (req.user?.rola === 'redaktor') return true
    return { opublikowany: { equals: true } }
  },
  delete: ({ req }) => req.user?.rola === 'admin'
}

Zwrócenie warunku zamiast wartości logicznej to najlepszy pomysł w całym systemie. Warunek wnika do zapytania do bazy, więc czytelnik bez konta widzi wyłącznie opublikowane artykuły, a filtrowanie dzieje się po stronie bazy, nie po pobraniu wszystkiego do pamięci.

Reguły dotyczą wszystkich dróg dostępu naraz: panelu, API i wywołań lokalnych. To znaczy, że nie da się przypadkiem obejść uprawnień, wołając dane inną ścieżką, co przy systemach z osobno konfigurowanym API zdarza się regularnie.

Pojedyncze pola też mają własne reguły. Wewnętrzna notatka redakcyjna może być widoczna tylko dla zespołu, mimo że sam artykuł jest publiczny.

Lokalne API

Ponieważ system działa w tym samym procesie co aplikacja, treść pobierasz bez zapytania sieciowego.

Code
TypeScript
import { getPayload } from 'payload'
import config from '@payload-config'

export default async function StronaArtykulu({ params }) {
  const payload = await getPayload({ config })

  const wynik = await payload.find({
    collection: 'artykuly',
    where: { slug: { equals: params.slug } },
    limit: 1
  })

  return <article>{wynik.docs[0]?.tytul}</article>
}

To wywołanie idzie prosto do bazy danych, z pominięciem warstwy HTTP. Odpada serializacja, odpada opóźnienie sieciowe i odpada pytanie o adres API w różnych środowiskach.

Konsekwencja dla wydajności jest widoczna zwłaszcza przy generowaniu statycznym. Budowa tysiąca stron nie oznacza tysiąca zapytań sieciowych do własnego serwera, tylko tysiąc zapytań do bazy, co jest rzędy wielkości szybsze.

Klasyczne API również działa, więc aplikacja mobilna albo osobny frontend nadal mają czym się połączyć. Wybór między jednym a drugim zależy od tego, czy konsument treści działa w tym samym procesie.

Bloki, czyli budowanie stron z klocków

Kolekcja o sztywnych polach wystarcza przy blogu, a przestaje wystarczać przy stronach marketingowych, gdzie każda podstrona wygląda inaczej.

Typ pola złożonego z bloków rozwiązuje to bez rezygnacji z typowania. Definiujesz zestaw dostępnych sekcji, a redakcja układa z nich stronę w dowolnej kolejności.

Code
TypeScript
{
  name: 'sekcje',
  type: 'blocks',
  blocks: [
    {
      slug: 'naglowek',
      fields: [
        { name: 'tytul', type: 'text' },
        { name: 'podtytul', type: 'textarea' }
      ]
    },
    {
      slug: 'galeria',
      fields: [
        { name: 'zdjecia', type: 'upload', relationTo: 'media', hasMany: true }
      ]
    }
  ]
}

Po stronie frontendu każdy blok odpowiada jednemu komponentowi, a strona renderuje się przez przejście po liście. Typy wygenerowane z konfiguracji rozróżniają warianty, więc kompilator wskaże brakującą obsługę nowego bloku, zamiast pozwolić na cichą lukę w renderowaniu.

Jest tu pułapka warta odnotowania. Zbyt bogaty zestaw bloków zamienia system treści w edytor stron i redakcja zaczyna projektować układ zamiast pisać. Kilkanaście sekcji o jasnym przeznaczeniu daje lepszy efekt niż czterdzieści wariantów różniących się szczegółami.

Dobrym sprawdzianem jest nazewnictwo. Blok nazwany zgodnie z rolą na stronie, na przykład sekcja z opiniami klientów, zostanie użyty tam, gdzie powinien. Blok nazwany od wyglądu, na przykład dwie kolumny z obrazkiem po lewej, zostanie użyty wszędzie i po roku nikt nie odróżni od siebie ośmiu podobnych wariantów.

Hooki i logika biznesowa

Do każdej operacji na dokumencie da się podpiąć funkcję wykonywaną przed zapisem albo po nim. To miejsce na wszystko, co ma się dziać automatycznie.

Code
TypeScript
hooks: {
  beforeChange: [
    ({ data }) => {
      if (!data.slug && data.tytul) {
        data.slug = data.tytul.toLowerCase().replace(/\s+/g, '-')
      }
      return data
    }
  ],
  afterChange: [
    async ({ doc }) => {
      await fetch(`${process.env.APP_URL}/api/revalidate?slug=${doc.slug}`)
    }
  ]
}

Drugi z tych hooków rozwiązuje problem, który przy generowaniu statycznym pojawia się zawsze: strona zbudowana raz nie wie, że treść się zmieniła. Wywołanie odświeżenia po zapisie sprawia, że publikacja artykułu aktualizuje stronę w kilka sekund, bez przebudowy całego serwisu.

Warto trzymać w hookach wyłącznie logikę związaną z danymi. Wysyłka powiadomień, generowanie miniatur i podobne zadania lepiej wypadają jako zadanie w kolejce, bo hook wykonywany synchronicznie wydłuża czas zapisu widziany przez redakcję, a jego błąd potrafi zablokować publikację.

Wersje i wiele języków

Dwie funkcje, które przy poważnym serwisie decydują o wyborze systemu, a przy prototypie łatwo je przeoczyć.

Wersjonowanie zapisuje historię zmian dokumentu i pozwala wrócić do poprzedniego stanu. Osobno działa mechanizm szkiców, w którym opublikowana wersja i wersja robocza istnieją równolegle, więc redakcja przygotowuje zmiany bez wpływu na to, co widzi czytelnik.

Code
TypeScript
versions: {
  drafts: { autosave: { interval: 2000 } },
  maxPerDoc: 50
}

Ograniczenie liczby wersji na dokument warto ustawić świadomie, a nie odruchowo. Domyślnie system trzyma sto wersji dokumentu i sam usuwa najstarsze, więc historia nie rośnie w nieskończoność. Baza puchnie dopiero wtedy, gdy wpiszesz tam zero, co w tej konfiguracji znaczy „zachowaj wszystko", i połączysz to z autozapisem co dwie sekundy.

Wielojęzyczność działa na poziomie pojedynczego pola. Zaznaczasz, które pola są tłumaczone, a system trzyma dla nich wartość osobno dla każdego języka. Identyfikator, powiązania i data publikacji pozostają wspólne, więc nie powstają dwa niezależne dokumenty, które trzeba ręcznie synchronizować.

Przy pobieraniu treści podajesz język, a system zwraca odpowiednie wartości. Warto od razu ustalić, co ma się dziać przy brakującym tłumaczeniu: pokazać wersję podstawową czy ukryć dokument w danym języku. Domyślne zachowanie sprawdza się przy serwisie, w którym wszystko jest tłumaczone, i myli czytelnika przy serwisie tłumaczonym częściowo, gdzie tekst w obcym języku pojawia się bez ostrzeżenia pośród reszty. Rozstrzygnięcie tego na początku jest tańsze niż poprawianie po zgłoszeniu od redakcji.

Baza danych i hosting

System obsługuje bazy dokumentowe oraz relacyjne, a wybór ma realne konsekwencje.

Baza relacyjna daje migracje schematu, integralność powiązań i możliwość zapytań spoza systemu. Dobrze łączy się z warstwą dostępu opisaną w tekście o Prismie, jeśli część aplikacji sięga po dane bezpośrednio.

Baza dokumentowa jest wygodniejsza przy strukturach zagnieżdżonych i luźniejsza przy zmianach schematu. Kosztem jest brak wymuszonej spójności powiązań.

Przy wdrożeniu bezserwerowym warto pamiętać o dwóch rzeczach. Pierwsza to pula połączeń do bazy, bo każda instancja funkcji otwiera własne, a limit bywa osiągnięty szybciej, niż się wydaje. Rozwiązaniem jest pośrednik zarządzający połączeniami. Druga to przechowywanie plików, bo system plików funkcji jest ulotny, więc grafiki i załączniki muszą trafiać do magazynu obiektowego.

Pliki i grafika

Obsługa mediów jest częścią systemu, a nie dodatkiem, i warto skonfigurować ją zanim redakcja wgra pierwsze pięćset zdjęć.

Kolekcja z włączonym przesyłaniem plików przyjmuje pliki i generuje warianty rozmiarów według podanej listy. Wariant powstaje raz, przy wgrywaniu, więc strona nie przelicza obrazów przy każdym żądaniu.

Code
TypeScript
upload: {
  imageSizes: [
    { name: 'miniatura', width: 400 },
    { name: 'karta', width: 800 },
    { name: 'pelny', width: 1600 }
  ],
  mimeTypes: ['image/*']
}

Pole opisu alternatywnego warto ustawić jako wymagane. 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.

Docelowe miejsce przechowywania zmienia się przez wtyczkę, bez zmian w kodzie stron. Domyślnie pliki lądują na dysku, co wystarcza na własnym serwerze i zawodzi przy wdrożeniu bezserwerowym. Przejście na magazyn obiektowy to kilka linii konfiguracji, natomiast przeniesienie plików już wgranych wymaga osobnego skryptu, więc lepiej rozstrzygnąć to na starcie.

Rozmiar wariantów warto dobrać do rzeczywistego układu strony, a nie do okrągłych liczb. Trzy warianty odpowiadające trzem miejscom, w których zdjęcie faktycznie się pojawia, dają mniejszy rachunek za magazyn i szybszą stronę niż sześć wariantów na wszelki wypadek.

Payload kontra alternatywy

RozwiązanieMocna stronaSłabośćKiedy wybrać
PayloadKonfiguracja w kodzie, wspólna aplikacja z frontendemWymaga samodzielnego wdrożeniaProjekt na Next.js z zespołem programistów
StrapiPanel do budowy modelu, duża społecznośćOsobna usługa do utrzymaniaModel treści budowany bez programisty
SanityUsługa zarządzana, edycja na żywoRozliczenie za ruch i dokumentyRedakcja pracująca równolegle
ContentfulDojrzałość, wsparcie dla korporacjiWysoka cena, sztywny modelDuża organizacja z wymogami zgodności

Podział przebiega wzdłuż jednego pytania: kto definiuje strukturę treści. Jeśli robi to programista, konfiguracja w kodzie wygrywa, bo przechodzi przegląd i wersjonowanie. Jeśli ma to robić redakcja bez pomocy technicznej, panel graficzny jest właściwszy.

Drugie pytanie dotyczy wdrożenia. Usługi zarządzane zdejmują z Ciebie utrzymanie serwera i bazy, kosztem rachunku rosnącego z ruchem i uzależnienia od jednego dostawcy. Samodzielne uruchomienie odwraca ten układ.

Typowe błędy

Pierwszy to planowanie hostingu zarządzanego u twórców. Zapisy są wstrzymane, więc wdrożenie trzeba przewidzieć samodzielnie od początku projektu.

Drugi to pobieranie treści przez zapytanie HTTP do własnej aplikacji. Skoro system działa w tym samym procesie, wywołanie lokalne jest szybsze i prostsze.

Trzeci to poleganie na ukryciu pola w panelu jako zabezpieczeniu. Widoczność w interfejsie to nie to samo co reguła dostępu, a dane bez reguły wyjdą przez API.

Czwarty to brak migracji przy bazie relacyjnej. Zmiana pola w konfiguracji nie zmienia sama tabeli na produkcji, więc pominięcie migracji kończy się rozjazdem schematu.

Piąty to trzymanie plików w systemie plików przy wdrożeniu bezserwerowym. Po każdym wdrożeniu znikają, a odkrywa się to zwykle po tygodniu.

Szósty to obietnica złożona redakcji, że sama zmieni strukturę treści. W tym systemie zmiana modelu to zmiana kodu i wdrożenie, więc oczekiwania warto ustawić na początku.

FAQ

Czy Payload jest darmowy?

Tak, kod jest otwarty na licencji pozwalającej na dowolne użycie, także komercyjne, bez opłat licencyjnych. Płacisz wyłącznie za infrastrukturę: serwer albo funkcje, bazę danych i magazyn plików.

Co się stało po przejęciu przez Figmę?

Projekt pozostał otwarty i rozwijany, a licencja się nie zmieniła. Wstrzymane zostały natomiast zapisy do usługi hostingu prowadzonej przez twórców, więc nowe wdrożenia trzeba uruchomić samodzielnie albo u wybranego dostawcy.

Czy potrzebuję Next.js?

Do pełnego wykorzystania wersji trzeciej tak, bo system instaluje się jako część aplikacji Next.js. Treść można natomiast konsumować z dowolnego miejsca przez API, więc aplikacja mobilna albo frontend w innej technologii nadal ma czym się połączyć.

Czy redakcja poradzi sobie bez programisty?

Codzienna praca redakcyjna tak, bo panel administracyjny jest kompletny i przyjazny. Zmiana struktury treści wymaga jednak edycji kodu i wdrożenia, więc przy zespole bez wsparcia technicznego lepszy bywa Strapi albo usługa zarządzana.

Jaką bazę wybrać?

Relacyjną, jeśli zależy Ci na migracjach schematu, integralności powiązań i możliwości zapytań spoza systemu. Dokumentową, jeśli struktury są mocno zagnieżdżone i często się zmieniają. Przy wątpliwościach relacyjna jest bezpieczniejszym domyślnym wyborem.

Dokumentacja stoi na stronie projektu, a kod w repozytorium na GitHubie.