Przejdź do treści
Intum Pomoc

Witryny, strony i szablony

Aktualizacja: 39 min czytania
Na tej stronie

Dokumentacja API do tworzenia stron CMS przez HTTP.

Autoryzacja: Wszystkie requesty wymagają Authorization: Bearer TOKEN

Ważne: Zamiast *_id można używać *_code (np. site_code zamiast site_id)

Zobacz też:

API Endpoints

Layouts (Szablony)

Metoda Ścieżka Opis
GET /cms/layouts.json Lista szablonów
GET /cms/layouts/:code.json Szczegóły (po code lub id)
POST /cms/layouts.json Utworzenie
PATCH /cms/layouts/:code.json Aktualizacja
PATCH /cms/layouts/:code/set_field.json Zapis jednego klucza fields szablonu (parametry jak w set_field strony, tylko wartości tekstowe) — wartość wspólna dla wszystkich stron z tym szablonem, w treści {{ layout.klucz }}
DELETE /cms/layouts/:code.json Usunięcie

Sites (Strony WWW)

Metoda Ścieżka Opis
GET /cms/sites.json Lista site’ów
GET /cms/sites/:code.json Szczegóły
POST /cms/sites.json Utworzenie
PATCH /cms/sites/:code.json Aktualizacja

Pages (Strony)

Metoda Ścieżka Opis
GET /cms/pages.json Lista stron
GET /cms/pages/:code.json Szczegóły
POST /cms/pages.json Utworzenie
PATCH /cms/pages/:code.json Aktualizacja
GET /cms/pages/:code/env.json Dane strony (env)
PATCH /cms/pages/:code/set_field.json Zapis jednego klucza fields (key, value, opcjonalnie locale; albo values: { "": domyślna, "en": ... } - kilka wersji językowych naraz; pusta wartość usuwa klucz). Tylko wartości tekstowe - value zapisuje się jako string, więc tablicy/obiektu (menu, cennik, FAQ) tak nie zmienisz, patrz “Edycja tablic w fields”

Paragraphs (Paragrafy)

Metoda Ścieżka Opis
GET /cms/paragraphs.json Lista paragrafów
GET /cms/paragraphs/:code.json Szczegóły
POST /cms/paragraphs.json Utworzenie
PATCH /cms/paragraphs/:code.json Aktualizacja
PATCH /cms/paragraphs/reorder.json Nowa kolejność paragrafów w boxie — {"ids": [12, 7, 30]}, zapisuje priority 1..N

Assets (Pliki)

Metoda Ścieżka Opis
GET /cms/assets.json Lista assetów
GET /cms/assets/:id.json Szczegóły
POST /cms/assets.json Utworzenie (obsługuje ZIP)
PATCH /cms/assets/:id.json Aktualizacja
DELETE /cms/assets/:id.json Usunięcie

Adres pliku jest stały: https://<host assetów>/<id konta>/cms/assets/<nazwa> - podmiana pliku pod tą samą nazwą nie zmienia URL-a (zmienia go zmiana name albo folderu, bo nazwa folderu wchodzi do name). Pole fields.cors włącza pobieranie pliku z innych domen (nagłówek Access-Control-Allow-Origin, potrzebny np. dla .wasm ładowanego przez aplikację); ustawić je może wyłącznie administrator systemu - innym użytkownikom API wycina ten klucz z fields.

Articles (Artykuły)

Metoda Ścieżka Opis
GET /cms/articles.json Lista artykułów
GET /cms/articles/:id.json Szczegóły
POST /cms/articles.json Utworzenie
PATCH /cms/articles/:id.json Aktualizacja
DELETE /cms/articles/:id.json Usunięcie

Categories (Kategorie artykułów)

Metoda Ścieżka Opis
GET /cms/categories.json Lista kategorii (filtry: site_id, page_id, query)
GET /cms/categories/:id.json Szczegóły
POST /cms/categories.json Utworzenie
PATCH /cms/categories/:id.json Aktualizacja
DELETE /cms/categories/:id.json Usunięcie (422, gdy kategoria jest główną jakiegoś artykułu)

Authors (Autorzy artykułów)

Metoda Ścieżka Opis
GET /cms/authors.json Lista autorów (filtr query po nazwie i opisie; user_id)
GET /cms/authors/:id.json Szczegóły
POST /cms/authors.json Utworzenie (zwykle niepotrzebne - nowa nazwa w author artykułu zakłada autora sama)
PATCH /cms/authors/:id.json Aktualizacja (opis, zdjęcie, strona, podpięcie pod użytkownika)
DELETE /cms/authors/:id.json Usunięcie - artykuły zostają, tracą tylko powiązanie (author_id), nazwa w author zostaje

Preview (Podgląd)

Metoda Ścieżka Opis
GET /w/:site_code Podgląd strony głównej
GET /w/:site_code/:path Podgląd strony

Pola modeli

Layout

Pole Typ Wymagane Opis
code string tak Unikalny kod (zalecany format: {site_code}-layout)
name string tak Nazwa
kind string tak page
content string tak Szablon HTML z {{ content }}
site_id integer tak ID site’a (zawsze podawaj przy tworzeniu layoutu)
example_content string nie Przykładowa treść do podglądu layoutu

Site

Pole Typ Wymagane Opis
code string tak Unikalny kod
name string tak Nazwa
kind string tak www lub kb
layout_code string nie Code layoutu
locale string nie Domyślny locale dla wszystkich stron site’a (np. pl, en, de, fr, es, cs, sk, uk). Ostatni fallback w chain — używany gdy strona nie ma własnego page.locale ani ?lang= ani locale domeny. Steruje którym wariantem językowym renderują się pola z locale-keyed fields (Page, Paragraph, Layout)
multilang boolean nie Włącza tryb SEO multilang — dodaje zmienne Liquid {{ html_lang }}, {{ canonical_url }}, {{ seo_alternates }}, {{ seo_head }} dostępne w layoucie, oraz 301 redirect path↔locale (patrz CMS Wielojęzyczność)

Page

Pole Typ Wymagane Opis
code string nie Unikalny kod (zalecany format: {site_code}-{path})
name string tak Nazwa
kind string tak text
path string tak URL path (np. “” dla głównej, “about”)
site_code string tak Code site’a
layout_code string nie Code layoutu
no_layout boolean nie Strona bez szablonu — renderuje samą swoją treść, bez własnego layoutu i bez domyślnego layoutu witryny (dla stron z własnym pełnym HTML)
content string nie Treść strony (Liquid)
paragraph_codes array nie Lista kodów paragrafów
priority decimal nie Priorytet sortowania
in_menu boolean nie Czy strona widoczna w menu (domyślnie: true)
menu_code string nie Kod grupy menu (np. “top_menu”, “footer_menu”)
redirect_to string nie URL przekierowania
in_sitemap boolean nie Czy strona ma być w sitemap.xml (domyślnie: true). Auto-ustawiane na false gdy ustawisz redirect_to, i z powrotem na true gdy redirect_to zostanie wyczyszczone. Można ręcznie nadpisać po stronie API jeśli potrzeba
locale string nie Własny locale strony (np. pl, en). Wygrywa nad ?lang=, domain.locale i site.locale — gdy ustawiony, strona zawsze renderuje się w tym języku. Decyduje którym wariantem językowym renderują się pola strony, paragrafów i layoutu z locale-keyed fields. Dla strony bez page.locale używane jest ?lang= z URL, potem domain.locale, potem site.locale (patrz “Wielojęzyczne pola”)

Paragraph

Pole Typ Wymagane Opis
code string tak Unikalny kod (zalecany format: {site_code}-{nazwa})
name string tak Nazwa
kind string tak text (treść) lub articles (lista artykułów)
name string nie Tytuł paragrafu (opcjonalny). text: pokazuje się jako <h3> nad treścią (gdy paragraf nie ma layoutu). articles: nagłówek <h2> nad listą
content string nie Treść (tylko kind: text)
markup string nie Format treści: html lub markdown (auto-detect jeśli puste; tylko kind: text)
site_code string nie Code site’a
page_code string nie Code strony (podłącza paragraf do strony)
box_code string nie Kod boxa (do grupowania paragrafów, np. “sidebar”, “footer”)
page_id integer nie ID strony, do której paragraf należy. Puste = paragraf wspólny dla całej witryny (pokazuje się w swoim boxie na każdej stronie)
priority decimal nie Priorytet sortowania. Nowy paragraf bez jawnego priority trafia na koniec swojego boxa
system_template string nie Klucz gotowego szablonu prezentacji (nazwa analogiczna do Cms::Layout). text: framed/highlight/hero; articles: default/cards/compact. Nieznany/pusty nie jest zapisywany — prezentacja domyślna
template_content string nie Własny markup Liquid prezentacji (“Dostosuj” w edytorze) — wygrywa z system_template i layoutem. Dla articles warianty w blokach <list>...</list> i <show>...</show>. Pusty lub identyczny z bazowym jest usuwany; błąd składni Liquid blokuje zapis (422)
layout_id integer nie Layout paragrafu (Cms::Layout kind paragraph) — wielokrotnego użytku szablon prezentacji, patrz sekcja “Szablony prezentacji paragrafów”

kind: articles — lista artykułów. Tytuł nad listą to pole name paragrafu (opcjonalne). Zamiast content paragraf ma konfigurację w fields:

Pole w fields Typ Wymagane Opis
category_codes array nie Kody kategorii (code z Cms::Category), z których brane są artykuły - artykuł wchodzi, gdy ma którąś z nich. Puste = wszystkie kategorie
per_page integer nie Ile najnowszych artykułów pokazać. Niepodany nie jest zapisywany w fields, a lista pokazuje domyślne 15

Paragraf renderuje nagłówek <h2> z name i najnowsze opublikowane artykuły z podanych kategorii. Każdy szablon ma dwa warianty markupu (Cms::Paragraph::ArticlesRenderer::TEMPLATES): list (artykuł na liście) i show (szczegóły). Zmienne Liquid jak w <cms type="article">. Gdy URL wskazuje artykuł (/strona/artykuł), pierwszy paragraf articles na stronie renderuje szczegóły artykułu wariantem show zamiast listy — strona z samym paragrafem (bez tagu <cms type="article">) ma więc działający widok szczegółów.

Szablon prezentacji (dla text i articles) wybiera się kolumnami system_template / template_content / layout_id — patrz sekcja “Szablony prezentacji paragrafów”.

Asset

Pole Typ Wymagane Opis
name string tak Nazwa pliku (unikalna w ramach konta)
kind string nie image, css, js, zip (auto-detect z rozszerzenia)
file file nie Plik do wgrania
folder_id integer nie ID folderu
site_id integer nie ID site’a
layout_id integer nie ID layoutu

Import ZIP: Wgranie pliku ZIP z kind: "zip" automatycznie rozpakuje archiwum zachowując strukturę folderów.

URL pliku: Po wgraniu asset dostępny jest pod s3_url (bezpośredni link S3). W treści stron i artykułów używaj stabilnego permalinku /cms/assets/ID/view — NIE s3_url (może wygasnąć) i NIE {{ asset 'nazwa' }} (nie działa jako tag Liquid).

Article

Pole Typ Wymagane Opis
title string tak Tytuł artykułu
path string auto Ścieżka URL (auto-generowana z tytułu jeśli pusta)
category_id integer nie ID kategorii głównej (kanonicznej) - decyduje o adresie kanonicznym artykułu. Można zdjąć (null). Artykuł bez żadnej kategorii pokazuje tylko strona z tagiem bez category_code (blog ogólny); gdy witryna takiej nie ma, artykuł publicznie daje 404, a podgląd w admince ostrzega i ma przycisk Dodaj stronę /blog (POST /cms/articles/:id/ensure_listing_page). Tekst zamiast liczby = nazwa nowej kategorii (tak działa opcja „Dodaj nową…” w formatce) - w API używaj category_code
category_code string nie Kod kategorii głównej - wygodniejszy zamiennik category_id. Kod, którego nie ma, zakłada nową kategorię na witrynie artykułu (nazwa = kod). W odpowiedzi: kod głównej
category_codes array nie Wszystkie kategorie artykułu (kody, główna pierwsza). W zapisie zastępuje zestaw; gdy artykuł nie ma jeszcze głównej, główną zostaje pierwsza z listy. Przyjmuje też string "news,poradnik"
category_ids array nie To samo po ID (formatka: checkboxy Kategorie)
locale string nie Język artykułu (pl, en, …). Na witrynie z multilang lista pokazuje tylko język domeny i artykuły bez locale
fields object nie Własne pola artykułu
author string nie Nazwa autora. Nazwa, której nie ma w /cms/authors.json, zakłada autora przy zapisie (dopasowanie do istniejącego bez względu na wielkość liter - "fakturownia" trafia w "Fakturownia"). Puste = artykuł bez autora. W odpowiedzi także author_id (ID rekordu z /cms/authors). Opis, zdjęcie i stronę autora ustawiasz na jego rekordzie, nie na artykule
abstract text nie Streszczenie/lead - zajawka na liście, fallback meta description, opis w OG i JSON-LD
summary text nie „W skrócie” (answer-first) - box nad treścią artykułu. Puste = brak boxa. Nie trafia do meta description ani do domyślnej zajawki na liście (w szablonie {{ summary }} jest dostępne wszędzie) - patrz „Streszczenie, W skrócie i Opis HTML”
content text nie Pełna treść
markup string nie Format treści: html lub markdown (auto-detect jeśli puste)
image_url string nie URL obrazka głównego
published_at datetime tak Data publikacji (domyślnie: teraz)
publish_to datetime nie Data zakończenia publikacji
accepted boolean nie Zaakceptowany (domyślnie: true)
published boolean nie Opublikowany (domyślnie: true)
site_id integer tak* Witryna artykułu - jej strony go pokazują. *Gdy konto ma dokładnie jedną witrynę, można pominąć (dobiera się sama); przy kilku brak = 422 z błędem na site_id. Kategoria główna musi należeć do tej samej witryny
html_title string nie Tytuł HTML (meta title)
html_description text nie Opis HTML (meta description)
html_keywords string nie Słowa kluczowe (meta keywords)

Streszczenie, W skrócie i Opis HTML:

Trzy pola opisowe, które łatwo pomylić. Każde ma jednego odbiorcę - nie powielaj tej samej treści:

Pole Etykieta w formatce Gdzie trafia
abstract Streszczenie zajawka na liście artykułów, meta description (gdy html_description puste), OG/Twitter, description w JSON-LD BlogPosting
summary W skrócie tylko box <div class="cms-article-summary"> nad treścią artykułu + {{ summary }} w szablonie; description w llms.txt (fallback na abstract, gdy puste); wyszukiwarka artykułów
html_description Opis HTML meta description wprost - wygrywa z abstract

summary jest pisane pod odpowiedź (answer-first, dla wyszukiwarek i modeli), abstract pod kliknięcie. Świadomie nie idzie do meta ani na listę - inaczej box na górze artykułu byłby kopią meta opisu, a o to właśnie chodziło w rozdzieleniu tych pól.

Renderuje się markdownem/HTML-em jak treść artykułu (zależnie od markup). Opcjonalność załatwia sama treść: puste pole = boxa nie ma, więc nie ma osobnego przełącznika „pokazuj/nie pokazuj”. Szablon, który chce własny wygląd, bierze {{ summary }} i renderuje po swojemu.

PATCH /cms/articles/123.json
{
  "article": { "summary": "Fakturę korygującą wystawisz w 3 krokach: ..." }
}

Kategorie i adresy (zadanie #1378):

Kategoria to rekord (/cms/categories.json), nie string. Artykuł ma kilka kategorii i co najwyżej jedną główną (category_id) - opcjonalną (artykuły można pisać przed ustawieniem stron witryny; bez kategorii pokazuje je tylko blog ogólny). Nie ma już osobnego „tematu” (topic, fields.category): dawny temat to dziś dodatkowa kategoria z path.

Wymiar Pole Co robi
kategoria główna category_id / category_code wyznacza adres kanoniczny: /<strona kanoniczna kategorii>/<path kategorii?>/<slug>. Gdy żadna strona witryny jej nie listuje, system sam zakłada stronę /blog z <cms type="article"/> (albo dopisuje ten tag do istniejącego /blog) i podpina ją jako kanoniczną
kategorie dodatkowe category_codes / category_ids artykuł pokazuje się na każdej stronie, której tag wymienia którąś z jego kategorii
path kategorii pole kategorii segment w adresie artykułu i podstrona listująca /<strona>/<path> (chipy {{ topics }}, sitemapa). Kategoria bez path = artykuł wprost pod stroną
język locale wersja językowa; filtrowana automatycznie na witrynie z multilang

Który segment wchodzi do adresu: category_code tagu strony tylko filtruje, które artykuły się pokazują - segment bierze się z kategorii samego artykułu: kategoria główna, gdy ma path, inaczej pierwsza dodatkowa z path w kolejności priority (mniejszy pierwszy), remis po id. Żadna kategoria artykułu nie ma path → adres wprost pod stroną. Dzięki temu jedna kategoria z path (np. poradnik) daje prefiks wszystkim swoim artykułom pod każdą stroną, bez wymieniania jej w tagu. Przykład: strona /page1 z tagiem category_code="news,poradnik", news ma path: news, poradnik ma path: poradnik:

Artykuł Kategorie Adres
a1 news (główna) /page1/news/a1
a2 poradnik (główna), temat1 /page1/poradnik/a2
a3 temat1 (główna, ma path), nielistowana przez /page1 /blog/temat1/a3 (blog ogólny, tag bez category_code)
a4 blog (główna, bez path) /blog/a4
a5 blog (główna, bez path), poradnik /blog/poradnik/a5 - segment z dodatkowej, choć /blog nie wymienia poradnik

Adres z innym segmentem albo pod inną stroną listującą (np. /blog/a1) robi 301 na kanoniczny. ?topic=<kod> na stronie listującej: kategoria z path → 301 na /<strona>/<path>, bez path → zawężenie listy w miejscu.

Nie koduj języka w kodzie kategorii (blog-en) - każdy kod to osobna sekcja.

PATCH /cms/articles/123.json
{
  "article": { "category_code": "blog", "category_codes": ["blog", "porady"], "locale": "pl" }
}

Zgodność wstecz: category_code w JSON nadal jest (kod głównej), a stare ?category= / ?topic= w adresach witryny działają. Pole topic i fields["category"] zostały usunięte - migracja zrobiła z każdego tematu kategorię z path równym kodowi.

Logika publikacji:

  • Artykuł jest widoczny gdy: published = true AND published_at jest w przeszłości AND (publish_to jest null LUB publish_to jest w przyszłości)
  • Gdy accepted = false, automatycznie ustawiane jest published = false

Konwencja nazewnictwa: Aby uniknąć konfliktów między site’ami, używaj prefiksu {site_code}- w kodach:

  • Layout: strona1-layout
  • Page: strona1-home, strona1-about
  • Paragraph: strona1-intro, strona1-footer

Category (kategoria artykułów)

Pole Typ Wymagane Opis
name string tak Nazwa pokazywana na witrynie ({{ c.name }} w menu i chipach)
code string tak Kod - unikalny w witrynie (ten sam kod może istnieć na dwu witrynach). Używany w category_code tagu i artykułu
path string nie Segment adresu: artykuły dostają /<strona>/<path>/<slug>, a lista podstronę /<strona>/<path>. Unikalny w witrynie. Puste = kategoria nie zmienia adresów
priority decimal nie Kolejność kategorii (domyślnie 1, mniejszy pierwszy, remis po id): w {{ categories }}/{{ topics }}, w liście kategorii artykułu i przy wyborze segmentu adresu, gdy główna nie ma path, a kilka dodatkowych ma
site_id integer tak* Witryna - jej strony listują kategorię. *Gdy konto ma dokładnie jedną witrynę, można pominąć (dobiera się sama); przy kilku brak = 422 z błędem na site_id. Starsze kategorie mogą mieć puste site_id (sprzed wymagalności) - działają na każdej witrynie, ale ich edycja wymaga już wybrania witryny
page_id integer nie Strona kanoniczna - pod nią mają adres kanoniczny artykuły z tą kategorią główną. Musi listować kategorię (tag bez category_code albo z jej kodem); puste = pierwsza strona witryny listująca kategorię. Wypełnia się samo przy automatycznym /blog
description text nie Opis ({{ c.description }} w Liquid)
fields object nie Własne pola

JSON kategorii nie zawiera account_id. DELETE kategorii, która jest główną jakiegoś artykułu, zwraca 422 - najpierw zmień tym artykułom kategorię główną; powiązania dodatkowe kasują się same.

POST /cms/categories.json
{ "category": { "name": "Porady", "code": "porady", "path": "porady", "site_id": 5 } }

Author (autor artykułów)

Centralna lista autorów konta (wspólna dla witryn). Nazwa jest unikalna w koncie bez względu na wielkość liter. Autor nie musi być użytkownikiem Intuma (gość, agencja, redakcja), ale może - wtedy puste name/description/avatar_url biorą się z profilu użytkownika, a wpisane wprost nadpisują profil.

Pole Typ Wymagane Opis
name string tak Nazwa - ta sama, którą wpisujesz w author artykułu. Unikalna w koncie (bez względu na wielkość liter)
description text nie Opis publiczny pod artykułem i w schema Person
avatar_url string nie URL zdjęcia (musi być http/https)
url string nie Strona autora - profil, blog, LinkedIn (http/https); trafia do sameAs w schema
user_id integer nie ID użytkownika Intuma - podpięcie pod profil (nazwa, opis, avatar z profilu, gdy własne puste)
fields object nie Własne pola

W odpowiedzi dodatkowo wyliczone display_name, display_description, display_avatar_url (wartość własna albo z profilu użytkownika). JSON nie zawiera account_id.

Nie zakładaj autora osobnym POST, gdy dodajesz artykuł - wystarczy author w artykule, rekord powstanie sam. POST /cms/authors.json ma sens, gdy od razu chcesz nadać opis/zdjęcie, albo gdy autor ma być podpięty pod użytkownika:

POST /cms/authors.json
{ "author": { "name": "Jan Kowalski", "description": "Redaktor bloga", "user_id": 12 } }

Tak samo z kategoriami: category_code w artykule z kodem, którego nie ma, zakłada kategorię na witrynie artykułu (nazwa = kod z wielkiej litery). POST /cms/categories.json jest potrzebny, gdy kategoria ma mieć własny path, page_id lub opis albo ma powstać na innej witrynie.

Liquid Templates

Strony używają szablonów Liquid. Dostępne zmienne:

Zmienna Opis
{{ name }} Nazwa strony
{{ path }} Ścieżka strony z / (np. /, /about)
{{ url }} Pełny URL strony
{{ content }} Treść (w layoucie - miejsce na content strony)
{{ paragraphs }} Lista paragrafów przypisanych do strony
{{ pages }} Lista wszystkich stron w site (do budowy menu)
{{ page }} Obiekt aktualnie wyświetlanej strony
{{ site }} Obiekt site’a (site.name, site.code, site.description, site.url)
{{ layout }} Obiekt layoutu (layout.name, layout.code, layout.description)
{{ categories }} Kategorie artykułów witryny z opublikowanymi artykułami, w kolejności priority, potem id - do menu bloga (code, name, path, description, url, current)
{{ topics }} Kategorie z path listowane przez tę stronę, w kolejności priority, potem id - do chipów nad listą (code, name, path, url, current)
{{ year }} Aktualny rok
{{ powered_by }} “Intum”
{{ preview_mode }} true jeśli strona jest w trybie podglądu
{{ base_path }} Prefiks do budowania linków ({{ base_path }}/kontakt): w preview /w/<kod_witryny>, na domenie klienta path_prefix witryny (zwykle pusty)

Właściwości obiektów

Page (page, elementy pages):

  • id - ID strony
  • name - nazwa strony
  • title - tytuł strony (z fields lub name)
  • path - ścieżka (w preview: /w/site/path, normalnie: /path)
  • raw_path - surowa ścieżka z bazy, bez prefiksów (np. rejestr-zmian) - do porównań w Liquid ({% if page.raw_path == ‘rejestr-zmian’ %})
  • url - pełny URL
  • site_id - ID site’a
  • html_title - tytuł HTML
  • html_description - opis HTML (meta description)
  • html_keywords - słowa kluczowe (meta keywords)
  • html_script - skrypt HTML

Site:

  • name - nazwa site’a
  • code - kod site’a
  • description - opis
  • url - URL site’a

Layout:

  • name - nazwa layoutu
  • code - kod layoutu
  • description - opis
  • content - treść szablonu

Własne pola (fields)

Każdy obiekt (Page, Site, Layout) może mieć własne pola w fields. Dostęp do nich w szablonach:

{{ page.owner }}        → page.fields["owner"]
{{ page.html.title }}   → page.fields["html"]["title"] (zagnieżdżone)
{{ owner }}             → page.fields["owner"] (skrót - tylko dla page)
{{ html.title }}        → page.fields["html"]["title"] (skrót)

{{ site.company }}      → site.fields["company"]
{{ layout.version }}    → layout.fields["version"]

Nazwa pola w content musi być poprawną zmienną Liquida ([A-Za-z_][\w-]*). Tekst w klamrach ({{ Nasz zespół }}, całe zdanie) przy zapisie rekordu zamienia się na klucz z pierwszych słów ({{ nasz_zespol }}, w layoucie {{ layout.nasz_zespol }}), a tekst trafia do fields[klucz] jako wartość początkowa (istniejący klucz nie jest nadpisywany). Tworząc treść przez API pisz od razu {{ klucz }} + fields.

Przykład użycia w layoucie:

<!DOCTYPE html>
<html>
<head>
  <title>{{ page.html.title }} - {{ site.name }}</title>
  <meta name="author" content="{{ site.company }}">
</head>
<body>
  <header>
    <h1>{{ site.name }}</h1>
    <p>Wersja szablonu: {{ layout.version }}</p>
  </header>
  <main>{{ content }}</main>
  <footer>© {{ year }} {{ page.owner }}</footer>
</body>
</html>

Ustawianie fields przez API:

PATCH /cms/pages/strona1-home.jsonContent-Type: application/json

{
  "page": {
    "fields": {
      "owner": "Jan Kowalski",
      "html": {
        "title": "Strona główna"
      }
    }
  }
}

ANTYWZORZEC: cała strona w jednym polu (content: "{{ body }}")

Nigdy nie wrzucaj całego HTML-a strony do jednego pola fields i nie zostawiaj w content samego {{ body }}:

// ŹLE - cała strona (struktura + teksty + linki) siedzi w jednym polu, per język
{
  "page": {
    "content": "{{ body }}",
    "fields": {
      "en": { "body": "<section class=\"wrap\"><h1>A tiling desktop</h1>...5 kB HTML..." },
      "pl": { "body": "<section class=\"wrap\"><h1>Kafelkowy pulpit</h1>...5 kB HTML..." }
    }
  }
}

Dlaczego to boli:

  • Duplikacja markupu - każdy język to osobna kopia całego HTML-a. Zmiana klasy CSS albo dołożenie przycisku to ta sama poprawka zrobiona N razy, a przy N+1 języku rozjazd jest kwestią czasu
  • Edytor pola staje się edytorem HTML - użytkownik, który chce poprawić jedno zdanie, dostaje kilka kilobajtów znaczników i łatwo zepsuje strukturę
  • Nie da się zmienić układu bez ruszania tłumaczeń (i odwrotnie)
  • Nic nie jest adresowalne - nie podmienisz przez API samego nagłówka czy adresu CTA, bo nie ma takiego klucza; trzeba przepisać całe pole

Dobrze: content to szablon (HTML + Liquid), fields to wszystkie teksty, linki i listy - per język tylko to, co faktycznie się tłumaczy:

{
  "page": {
    "content": "<section class=\"wrap\">\n  <h1>{{ hero_title }}</h1>\n  <p class=\"lead\">{{ hero_lead }}</p>\n  <a class=\"btn\" href=\"{{ demo_url }}\">{{ cta_demo }}</a>\n  {% for card in cards %}<div class=\"card\"><h3>{{ card.title }}</h3><p>{{ card.text }}</p></div>{% endfor %}\n</section>",
    "fields": {
      "demo_url": "/run",
      "en": {
        "hero_title": "A tiling desktop, in a browser tab",
        "hero_lead": "Tiles split the screen instead of overlapping.",
        "cta_demo": "Try the live demo",
        "cards": [{ "title": "BSP tiling", "text": "Tiles divide the screen." }]
      },
      "pl": {
        "hero_title": "Kafelkowy pulpit, w karcie przeglądarki",
        "hero_lead": "Kafelki dzielą ekran zamiast na siebie nachodzić.",
        "cta_demo": "Wypróbuj demo na żywo",
        "cards": [{ "title": "Układ BSP", "text": "Kafelki dzielą ekran." }]
      }
    }
  }
}

Zasady podziału:

  • W content tylko markup: tagi, klasy, atrybuty, ikony SVG, {% for %}/{% if %}. Ani jednego zdania widocznego dla użytkownika
  • W fields teksty, adresy i listy: nagłówki, akapity, napisy przycisków, href-y, identyfikatory osadzanych formularzy, tablice kart/FAQ/cennika
  • Wartość, która nie zależy od języka (adres GitHuba, /run, kod formularza wspólny dla wszystkich wersji) idzie top-level, poza sub-hasze pl/en - działa wtedy jako fallback dla każdego locale i zapisujesz ją w jednym miejscu
  • Krótki fragment HTML wewnątrz zdania (<strong>, <a href>, <span class="kbd">) zostaje w polu razem z tekstem - szyk zdania bywa inny w każdym języku, więc nie da się go rozbić na kawałki w szablonie
  • Nazwa klucza musi być poprawną zmienną Liquida i mówić o roli, nie o wyglądzie (hero_lead, cta_demo, signup_form_id - nie text1, bold_blue)

Strony dziedziczące (based_on_page_id) korzystają z tego wprost: slave ma puste content i puste fields, a bierze szablon i teksty od mastera wg swojego locale.

Przerabianie istniejącej strony z {{ body }} na szablon + fields: zrób kopię aktualnego content i fields, wytnij teksty do kluczy, wyślij content i fields jednym PATCH, a potem porównaj wyrenderowany HTML sprzed i po zmianie (pomijając białe znaki) - powinien wyjść identyczny.

Tablice w fields (zalecane dla list i cenników)

Dla dynamicznych treści jak cenniki, listy funkcji, FAQ - zalecane jest używanie tablic w fields. Ułatwia to edycję danych bez modyfikacji HTML.

Przykład: cennik z planami:

PATCH /cms/pages/cennik.jsonContent-Type: application/json

{
  "page": {
    "fields": {
      "plans": [
        { "name": "Micro", "price": "0", "users": "1", "popular": false },
        { "name": "Start", "price": "49", "users": "2", "popular": true },
        { "name": "Pro", "price": "149", "users": "5", "popular": false }
      ],
      "faq": [
        { "question": "Czy mogę zmienić plan?", "answer": "Tak, w dowolnym momencie." },
        { "question": "Czy jest okres próbny?", "answer": "Tak, 14 dni za darmo." }
      ]
    },
    "content": "... szablon Liquid używający plans i faq ..."
  }
}

Szablon Liquid (content):

<h1>Cennik</h1>
<div class="plans">
{% for plan in plans %}
  <div class="plan {% if plan.popular %}popular{% endif %}">
    <h3>{{ plan.name }}</h3>
    <p>{{ plan.price }} zł/mies.</p>
    <p>{{ plan.users }} użytkowników</p>
  </div>
{% endfor %}
</div>

<h2>FAQ</h2>
{% for item in faq %}
  <div class="faq-item">
    <h4>{{ item.question }}</h4>
    <p>{{ item.answer }}</p>
  </div>
{% endfor %}

Ważne: Pola z fields są dostępne bezpośrednio w Liquid (np. plans, faq), nie przez fields.plans.

Zagnieżdżone tablice (np. features w planie):

{
  "plans": [
    {
      "name": "Pro",
      "features": ["CRM", "Fakturowanie", "API", "Priorytetowe wsparcie"]
    }
  ]
}
{% for plan in plans %}
  <h3>{{ plan.name }}</h3>
  <ul>
  {% for feature in plan.features %}
    <li>{{ feature }}</li>
  {% endfor %}
  </ul>
{% endfor %}

Warunki w pętli:

{% for plan in plans %}
  {% if plan.popular %}
    <div class="plan highlighted">{{ plan.name }} - Popularny!</div>
  {% else %}
    <div class="plan">{{ plan.name }}</div>
  {% endif %}
{% endfor %}

Zalety używania tablic w fields:

  • Łatwa edycja danych bez dotykania HTML
  • Możliwość zmiany cen, nazw, opisów przez API
  • Czytelna struktura danych
  • Możliwość dynamicznego dodawania/usuwania elementów

Edycja tablic w fields (dodanie pozycji menu, planu, pytania FAQ):

  • Nie używaj set_field - zapisuje value jako tekst, tablica zamieni się w string i pętla for przestanie działać
  • Zrób GET rekordu, zmień tablicę lokalnie i wyślij całe fields przez zwykły PATCH: PATCH /cms/pages/:code.json z {"page": {"fields": {...}}} albo PATCH /cms/layouts/:code.json z {"layout": {"fields": {...}}}
  • PATCH z fields podmienia cały jsonb - odeślij wszystkie klucze i wszystkie sub-hashe językowe (pl, en, …), nie tylko ten zmieniany. Po zapisie porównaj fields z odpowiedzi z tym, co wysłałeś
  • Na witrynie wielojęzycznej tablica zwykle jest osobno w każdym locale (fields.pl.nav_items, fields.en.nav_items) - zmieniaj tylko wersje językowe, w których dodawana strona istnieje

Szablony paragrafów

Paragrafy mogą używać Liquid do dynamicznej treści. Dostępne zmienne:

Zmienna Opis
{{ name }} Nazwa paragrafu (z pola name, fallback do fields["name"])
{{ description }} Opis paragrafu (z pola description, fallback do fields["description"])
{{ dowolne_pole }} Wartość z fields["dowolne_pole"]

Przykład paragrafu z fields:

POST /cms/paragraphs.json
{
  "paragraph": {
    "code": "strona1-team-jan",
    "name": "Jan Kowalski",
    "kind": "text",
    "content": "<div><h3>{{ name }}</h3><p>{{ position }}</p></div>",
    "site_code": "strona1",
    "box_code": "team",
    "fields": {
      "position": "Senior Developer"
    }
  }
}

Szablony prezentacji paragrafów (system_template / template_content)

Prezentację paragrafu wybiera się i dostosowuje przez kolumny system_template (klucz gotowego szablonu, nazwa analogiczna do Cms::Layout), template_content (własny markup) i layout_id (layout wielokrotnego użytku) — to działa tak samo z API, jak z edytora WYSIWYG (zakładka Szablon). Pełny opis systemu paragrafów: CMS Paragrafy.

Hierarchia nadpisań (silniejszy wygrywa): template_content (własny markup) > layout_id (layout) > system_template (gotowy szablon) > prezentacja domyślna.

Paragraf text — treść można opakować gotowym szablonem albo własnym markupem Liquid:

Pole Opis
system_template framed (ramka), highlight (wyróżnienie z paskiem), hero (baner, wyśrodkowany). Brak/nieznany = domyślna prezentacja: opcjonalny <h3> z name + treść + zdjęcie
template_content Własny markup Liquid — wygrywa z system_template i layoutem
fields.image_url Załącznik (zdjęcie) paragrafu — publiczny URL. Domyślna prezentacja i gotowe szablony renderują go pod treścią ({% if image_url != blank %}). Upload pliku: POST /cms/assets.json (multipart, asset[kind]=image) → do fields.image_url wpisz s3_url z odpowiedzi; formatka CRUD ma też pole paragraph[image] (plik), które robi to samo

Szablon dostaje zmienne: {{ content }} (wyrenderowana treść paragrafu — po Liquid i konwersji markdown), {{ name }}, {{ description }}, {{ image_url }} oraz własne pola z fields. Markupy gotowych szablonów: Cms::Paragraph::ParagraphRenderer::TEMPLATES.

POST /cms/paragraphs.json
{
  "paragraph": {
    "code": "strona1-promo",
    "name": "Promocja",
    "kind": "text",
    "content": "Do końca miesiąca **-20%** na wszystko.",
    "markup": "markdown",
    "site_code": "strona1",
    "box_code": "main",
    "system_template": "highlight"
  }
}

Własny szablon (odpowiednik przycisku “Dostosuj”):

PATCH /cms/paragraphs/123.json
{
  "paragraph": {
    "template_content": "<aside class=\"promo\" style=\"background:#fef3c7;padding:16px;\">{% if name != blank %}<h3>{{ name }}</h3>{% endif %}{{ content }}</aside>"
  }
}

Paragraf articles — szablon ma dwa warianty: list (artykuł na liście) i show (widok szczegółów po kliknięciu):

Pole Opis
system_template default, cards (z obrazkiem image_url), compact (data + link)
template_content Własny markup Liquid z wariantami w blokach <list>...</list> i <show>...</show> (markup bez bloków = sam wariant listy) — wygrywa z system_template i layoutem

Zmienne w obu wariantach (podzbiór <cms type="article">): {{ id }}, {{ title }}, {{ path }} (link do artykułu), {{ author }}, {{ category_code }}, {{ abstract }}, {{ summary }} („w skrócie”), {{ content }}, {{ image_url }}, {{ published_at }}, {{ published_at_date }}, {{ published_at_iso }}, {{ updated_at_iso }}, {{ back_url }} (powrót na stronę z listą), {{ fields }} (własne pola artykułu) oraz pozycja wpisu na liście: {{ index }} (od zera), {{ position }} (od jedynki), {{ total }}, {{ first }}, {{ last }}. Markupy gotowych szablonów: Cms::Paragraph::ArticlesRenderer::TEMPLATES.

POST /cms/paragraphs.json
{
  "paragraph": {
    "code": "strona1-aktualnosci",
    "name": "Aktualności",
    "kind": "articles",
    "site_code": "strona1",
    "page_code": "strona1-home",
    "box_code": "news",
    "system_template": "cards",
    "template_content": "<list>\n<div class=\"news-row\"><time>{{ published_at_date }}</time> <a href=\"{{ path }}\">{{ title }}</a></div>\n</list>",
    "fields": { "category_codes": ["aktualnosci"], "per_page": 5 }
  }
}

Layout paragrafu jako szablon prezentacji. Zamiast gotowego szablonu paragraf może wskazywać layout_id (Cms::Layout kind paragraph) — wielokrotnego użytku, wspólny dla wielu paragrafów:

  • text: treść layoutu to pełny szablon — dostaje {{ content }} (wyrenderowaną treść paragrafu) i pola paragrafu; system_template wtedy nie gra,
  • articles: layout może definiować warianty w blokach <list>...</list> i <show>...</show> — wygrywa z system_template, przegrywa z template_content,
  • formatka /cms/layouts przy kind paragraph ma przyciski wstawiające markup gotowych szablonów; w edytorze WYSIWYG layouty są w tym samym selekcie co gotowe szablony.

Zasady wspólne (normalizacja przy zapisie):

  • Pusty lub identyczny z bazowym własny markup (template_content) jest usuwany — zamrożona kopia nie dostawałaby przyszłych poprawek gotowego szablonu. Chcąc wrócić do gotowego szablonu, wyślij "template_content": null
  • Nieznany klucz system_template jest usuwany — renderuje się domyślna prezentacja
  • Błąd składni Liquid we własnym szablonie blokuje zapis (422, komunikat w errors.template_content). Na żywej stronie błąd Liquid degraduje do pustej treści, więc walidacja łapie go wcześniej
  • Stare klucze fields.template/fields.template_content są przy zapisie przenoszone do kolumn, a fields.template_list/template_show usuwane
  • Uwaga na cały jsonb: PATCH z fields podmienia całość — modyfikując jeden klucz (np. category_codes), odeślij też pozostałe (najpierw GET)

Wielojęzyczne pola

Pola fields w Page, Paragraph i Layout wspierają zagnieżdżone hashe z kluczem locale (pl, en, …). Site i Domain mają własne pole locale które wpływa na fallback chain.

Pełna dokumentacja wielojęzyczności (strategie, chain fallback, per-domain locale, page inheritance): CMS Wielojęzyczność

Iteracja po paragrafach

{% for p in paragraphs %}
  <div>
    <h3>{{ p.name }}</h3>
    <p>{{ p.content }}</p>
  </div>
{% endfor %}
<nav>
  <ul class="flex gap-4">
    {% for p in pages %}
      {% if p.path == page.path %}
        <li><a href="{{ p.path }}" class="font-bold text-blue-600">{{ p.name }}</a></li>
      {% else %}
        <li><a href="{{ p.path }}" class="text-gray-600 hover:text-blue-600">{{ p.name }}</a></li>
      {% endif %}
    {% endfor %}
  </ul>
</nav>

Uwaga: W trybie podglądu (/w/...) linki automatycznie prowadzą do podglądu, a pod własną domeną - do normalnych ścieżek.

Strony można przypisać do grup menu przez pole menu_code. Filtrowanie w Liquid wymaga użycia assign przed pętlą for:

{% assign top_menu = pages | where: "menu_code", "top_menu" %}
<nav>
  {% for p in top_menu %}
    <a href="{{ p.path }}">{{ p.name }}</a>
  {% endfor %}
</nav>

Przykład layoutu z wieloma menu:

<header>
  {% assign main_menu = pages | where: "menu_code", "main" %}
  {% for p in main_menu %}
    <a href="{{ p.path }}">{{ p.name }}</a>
  {% endfor %}
</header>

{{ content }}

<footer>
  {% assign footer_menu = pages | where: "menu_code", "footer" %}
  {% for p in footer_menu %}
    <a href="{{ p.path }}">{{ p.name }}</a>
  {% endfor %}
</footer>

Uwaga: Zmienna pages zawiera tylko strony z in_menu = true. Strony z in_menu = false nie pojawiają się w menu.

Menu nie musi pochodzić z pages - wiele szablonów trzyma linki jako tablice w fields layoutu (np. nav_items, product_dropdown_items, footer_product_items), per język:

{% for p in layout.nav_items %}
  <a href="{{ p.path }}">{{ p.name }}</a>
{% endfor %}

Zanim dodasz stronę do menu, sprawdź w content layoutu, skąd menu bierze linki:

  • pętla po pages → ustaw na stronie in_menu / menu_code
  • pętla po layout.<klucz> → dopisz pozycję ({"name": "...", "path": "/..."}, plus pola, których używa pętla, np. sub) do tablicy w fields layoutu - jak w “Edycja tablic w fields”. Samo in_menu na stronie nic wtedy nie zmieni

Ta sama tablica bywa użyta w kilku miejscach (menu desktopowe, mobilne, stopka) - dopisanie pozycji zmienia je wszystkie, więc przejrzyj każde użycie klucza w layoucie.

{{ categories }} (kategorie artykułów witryny) i {{ topics }} (kategorie z path, które ta strona listuje) zawierają tylko pozycje z opublikowanymi artykułami w języku domeny - żaden link nie prowadzi na pustą listę. Każda pozycja ma code, name, path, url i current:

Pole Opis
code Kod kategorii
name Nazwa kategorii (pole Nazwa w /cms/categories)
path Segment adresu kategorii (puste, gdy kategoria go nie ma)
description Opis kategorii (tylko categories)
url categories: adres listingu - strona, która listuje kategorię, plus /<path> gdy go ma (puste, gdy żadna strona jej nie listuje). topics: podstrona kategorii pod tą stroną (/<strona-bloga>/<path>)
current true gdy to właśnie ta kategoria jest wyświetlana (z tagu strony albo z adresu)
<nav class="blog-menu">
  {% for c in categories %}
    <a href="{{ c.url }}" class="{% if c.current %}active{% endif %}">{{ c.name | escape }}</a>
  {% endfor %}
</nav>

<div class="chips">
  {% for t in topics %}
    <a href="{{ t.url }}" class="{% if t.current %}active{% endif %}">{{ t.name | escape }}</a>
  {% endfor %}
</div>

Uwaga: name i description to surowe wartości z bazy - przy wypisywaniu używaj filtra escape.

Tag CMS box - renderowanie paragrafów z boxa

Tag <cms type="box" id="X"> wstawia paragrafy z danego site’a, które mają box_code = X. Paragrafy są sortowane po priority rosnąco (niższy = wyżej), przy równych priorytetach po id.

Składnia:

<cms type="box" id="sidebar"></cms>
<cms type="box" id="sidebar"/>
<cms type="box" id="banner" max="1"></cms>
<cms type="box" id="footer" scope="site"></cms>
Atrybut Opis
type Typ tagu (obecnie: box)
id Wartość box_code paragrafów do wstawienia
max Opcjonalnie: maksymalna liczba paragrafów (np. max="1" dla bannera). Pełny box nie pokazuje w trybie edycji przycisku dodania treści
scope page (domyślnie) albo site — patrz niżej

scope — zasięg boxa

Paragraf ma pole page_id: albo należy do konkretnej strony, albo jest wspólny dla całej witryny (page_id puste).

  • scope="page" (domyślnie) — box pokazuje paragrafy tej strony (page_id = ta strona) oraz wspólne (page_id puste). Paragraf dodany w tym boxie w trybie edycji WYSIWYG dostaje page_id bieżącej strony, więc box o tym samym kodzie na innej stronie go nie pokaże.
  • scope="site" — box wspólny dla całej witryny (stopka, nagłówek, belka cookies). Pokazuje tylko paragrafy bez page_id, i takie też zakłada w trybie edycji. Ten sam paragraf pojawia się wtedy na każdej stronie używającej tego boxa.

Boxy w layoucie, które mają wyglądać identycznie na wszystkich stronach, powinny mieć scope="site".

Przykład layoutu z sidebar i footer:

<!DOCTYPE html>
<html>
<head><title>{{ name }}</title></head>
<body>
  <nav>
    {% for p in pages %}
      <a href="{{ p.path }}" {% if p.path == page.path %}class="active"{% endif %}>{{ p.name }}</a>
    {% endfor %}
  </nav>

  <header>
    <cms type="box" id="banner" max="1"></cms>
  </header>

  <main>{{ content }}</main>

  <aside>
    <cms type="box" id="sidebar"></cms>
  </aside>

  <footer>
    <!-- stopka ta sama na każdej stronie - stąd scope="site" -->
    <cms type="box" id="footer" scope="site"></cms>
  </footer>
</body>
</html>

Tworzenie paragrafów z box_code przez API:

POST /cms/paragraphs.json
{
  "paragraph": {
    "code": "strona1-sidebar-promo",
    "name": "Promocja",
    "kind": "text",
    "content": "<div class='promo'>Sprawdź naszą ofertę!</div>",
    "site_code": "strona1",
    "box_code": "sidebar",
    "priority": 10
  }
}

Tagi CMS changelog - metryczka i rejestr zmian (BIP)

<cms type="changelog_page"></cms>
<cms type="changelog_site" per_page="50"></cms>

Oba tagi wymagają włączonego rejestru zmian witryny (Cms::Site#change_log) — bez niego znikają bez śladu. Działają w treści strony i w layoucie.

changelog_page — metryczka BIP strony. Podsumowanie z rejestru zmian (Cms::ChangeLog) strony i jej paragrafów: data wytworzenia, autor, data publikacji i osoba udostępniająca (pierwszy wpis rejestru), data ostatniej modyfikacji i osoba modyfikująca (ostatni wpis). Fallback do atrybutów strony, gdy rejestr włączono później. Markup: section.cms-changelog-page.

changelog_site — pełny rejestr zmian witryny. Publiczna tabela wpisów (publicly_visible): data, zdarzenie, tytuł, ścieżka, osoba; najnowsze pierwsze. Atrybut per_page (domyślnie 50, max 200). Markup: section.cms-changelog-site.

Własny markup (inner template, Liquid — jak w <cms type="article">, ale bez podziału <list>/<show>):

<!-- per wpis rejestru; zmienne: created_at, event, title, path, user_name -->
<cms type="changelog_site" per_page="20">
  <div class="wpis">{{ created_at }} — {{ event }}: {{ title }} ({{ user_name }})</div>
</cms>

<!-- jednorazowo; zmienne: created_at, author, published_at, published_by, updated_at, updated_by -->
<cms type="changelog_page">
  <p>Autor: {{ author }}, ostatnia zmiana: {{ updated_at }} ({{ updated_by }})</p>
</cms>

Szablon systemowy bip_prosty_1 ma changelog_page pod {{ content }}, a na stronie o ścieżce rejestr-zmian pokazuje changelog_site przed treścią (Liquid: {% if page.path == 'rejestr-zmian' %}).

Tag CMS article - lista i widok artykułów

Tag <cms type="article"> obsługuje zarówno wyświetlanie listy artykułów jak i pojedynczego artykułu. Automatycznie rozpoznaje czy pokazać listę czy szczegóły na podstawie ścieżki URL.

Jak to działa:

  • /blog → wyświetla listę artykułów (sekcja <list>)
  • /blog/moj-artykul → wyświetla pojedynczy artykuł (sekcja <show>)

Składnia:

<cms type="article" category_code="news" per_page="10">
  <list>
    <!-- szablon dla każdego artykułu na liście -->
    <div class="news-item">
      <img src="{{ image_url }}" alt="{{ title }}">
      <h3><a href="{{ path }}">{{ title }}</a></h3>
      <span class="author">{{ author }}</span>
      <time>{{ published_at_date }}</time>
      <p>{{ abstract }}</p>
    </div>
  </list>
  <show>
    <!-- szablon dla widoku pojedynczego artykułu -->
    <article>
      <h1>{{ title }}</h1>
      <span class="author">{{ author }}</span>
      <time>{{ published_at }}</time>
      <div class="content">{{ content }}</div>
      <a href="{{ back_url }}">← Powrót do listy</a>
    </article>
  </show>
</cms>
Atrybut Opis
type article
category_code Opcjonalnie: kody kategorii artykułów. Kilka kodów po przecinku (category_code="blog,radar") łączy je w jedną chronologiczną listę; artykuł wchodzi, gdy ma którąś z nich. Atrybut tylko filtruje - segment path w adresie daje kategoria samego artykułu (główna, inaczej pierwsza dodatkowa z path wg priority), niezależnie od tego, co wymienia tag. Brak atrybutu = wszystkie kategorie
topic Opcjonalnie: zawężenie na sztywno do jednej kategorii (kod albo path). topic="any" wyłącza zawężanie kategorią z URL-a (blok “najnowsze” obok chipów zostaje pełny)
mode list - zawsze lista, detail - zawsze widok artykułu. Domyślnie zależnie od adresu
per_page Opcjonalnie: artykułów na stronie (domyślnie: 10, max: 25)
paginate paginate="false" - lista bez paginacji (sekcja “najnowsze” na stronie głównej)
site Domyślnie lista pokazuje tylko artykuły witryny, na której stoi strona. site="any" pokazuje artykuły ze wszystkich witryn konta. Artykuł nie może być wspólny dla kilku witryn - ma jedną witrynę

Zawężanie kategorią z adresu: lista bez atrybutu topic czyta kategorię ze ścieżki (/blog/porady - podstrona kategorii z path) albo z parametru ?topic=porady (stare linki ?category=porady też działają; kategoria z path robi z nich 301 na ścieżkę). Linki do chipów buduje zmienna {{ topics }} (patrz “Menu kategorii i chipy kategorii z path”).

WAŻNE:

  • Artykuł renderuje się tylko na witrynie, do której należy (site_id) — artykuł innej witryny ani stary bez witryny nie pokaże się na liście, chyba że tag ma site="any"
  • Zmienne Liquid w template <list> i <show> są FLAT: {{ title }}, {{ path }}, {{ abstract }}, {{ summary }}, {{ content }}, {{ published_at }}, {{ image_url }}, {{ back_url }}, {{ author }} — NIE {{ article.title }}
  • Jeśli treść artykułu zawiera przykłady kodu Liquid ({{ }}, {% %}), owij ją w ... — bez tego CMS zwróci 500
  • Layout musi mieć ustawiony site_id — bez tego <cms type="article"> może nie renderować artykułów

Obrazki w treści:

  • Upload przez POST /cms/assets.json (multipart: asset[file]=@plik, asset[site_id]=ID)
  • Referencja: /cms/assets/ID/view — NIE {{ asset 'nazwa' }} (ten tag nie działa)
  • Unikaj URL-i s3.eu-west-1.amazonaws.com/attachments.intum.net/public-files/ — wygasają (403)

Paginacja:

Gdy artykułów jest więcej niż per_page, automatycznie wyświetlana jest paginacja. Nawigacja między stronami przez parametr ?page=N:

  • /blog → strona 1
  • /blog?page=2 → strona 2
  • /blog?page=3 → strona 3

Klasy CSS paginacji:

Klasa Element
cms-pagination Kontener <nav>
cms-pagination-prev Link “Poprzednia”
cms-pagination-next Link “Następna”
cms-pagination-page Link do numeru strony
cms-pagination-current Aktualny numer strony (span)
cms-pagination-ellipsis Wielokropek między numerami

Przykład HTML paginacji:

<nav class="cms-pagination">
  <a href="/blog" class="cms-pagination-prev">« Poprzednia</a>
  <a href="/blog" class="cms-pagination-page">1</a>
  <span class="cms-pagination-current">2</span>
  <a href="/blog?page=3" class="cms-pagination-page">3</a>
  <span class="cms-pagination-ellipsis">…</span>
  <a href="/blog?page=10" class="cms-pagination-page">10</a>
  <a href="/blog?page=3" class="cms-pagination-next">Następna »</a>
</nav>

SEO - meta tagi artykułu:

Gdy wyświetlany jest widok szczegółowy artykułu (/blog/moj-artykul), zmienne html_title, html_description, html_keywords są automatycznie nadpisywane wartościami z artykułu. Dzięki temu layout może używać:

<head>
  <title>{{ html_title }}</title>
  <meta name="description" content="{{ html_description }}">
  <meta name="keywords" content="{{ html_keywords }}">
</head>

I automatycznie będą wyświetlane:

  • Na liście (/blog) → wartości ze strony
  • Na artykule (/blog/moj-artykul) → wartości z artykułu (lub tytuł artykułu jeśli html_title puste)

Na tym samym widoku layout i body strony mają też cały artykuł pod {{ article.* }} - np. {{ article.summary }}, {{ article.abstract }}, {{ article.fields.pub_date }}. To jedyne miejsce z tym prefiksem: wewnątrz <list>/<show> te same zmienne są flat ({{ summary }}), bo tam kontekstem jest już pojedynczy artykuł.

Sekcje szablonu:

Sekcja Opis
<list>...</list> Szablon dla każdego artykułu na liście
<show>...</show> Szablon dla widoku pojedynczego artykułu

Jeśli nie podasz własnych szablonów, używane są domyślne.

Dostępne zmienne w szablonie:

Zmienna Lista Show Opis
{{ id }} ✓ ✓ ID artykułu
{{ title }} ✓ ✓ Tytuł
{{ path }} ✓ - Ścieżka URL (link do artykułu)
{{ author }} ✓ ✓ Autor
{{ category_code }} ✓ ✓ Kod kategorii głównej
{{ abstract }} ✓ ✓ Streszczenie (zajawka, meta description, JSON-LD)
{{ summary }} ✓ ✓ „W skrócie” - answer-first box nad treścią. Puste = pusty string, więc owijaj {% if summary != blank %}
{{ content }} ✓ ✓ Pełna treść
{{ image_url }} ✓ ✓ URL obrazka
{{ published_at }} ✓ ✓ Data publikacji sformatowana
{{ published_at_date }} ✓ ✓ Tylko data
{{ back_url }} - ✓ URL powrotu do listy
{{ category_name }} ✓ ✓ Nazwa kategorii głównej (do okruszków)
{{ category_url }} ✓ ✓ Adres listingu kategorii głównej - strona, która ją listuje, plus /<path> gdy go ma (do okruszków)
{{ categories }} ✓ ✓ Wszystkie kategorie artykułu, główna pierwsza: code, name, path, url, main ({% for c in categories %})
{{ index }} / {{ position }} ✓ - Pozycja wpisu na liście: od zera / od jedynki
{{ total }} ✓ - Ile wpisów na tej stronie listy
{{ first }} / {{ last }} ✓ - Czy to pierwszy / ostatni wpis - do układu „pierwszy inaczej niż reszta”

Domyślny HTML (lista):

<article class="cms-article-item">
  <h3><a href="blog/sciezka-artykulu">Tytuł artykułu</a></h3>
  <time datetime="2026-01-15T12:00:00Z">15 stycznia 2026</time>
  <p>Streszczenie artykułu...</p>
</article>

Domyślny HTML (show):

<article class="cms-article-detail">
  <h1>Tytuł artykułu</h1>
  <span class="author">Jan Kowalski</span>
  <time datetime="2026-01-15T12:00:00Z">15 stycznia 2026</time>
  <div class="content">Pełna treść...</div>
  <a href="/blog">Powrót do listy</a>
</article>

Przykład użycia - blog z listą i widokiem artykułu:

Strona z path: "blog" będzie obsługiwać:

  • /blog → lista artykułów
  • /blog/wprowadzenie-do-rails → widok artykułu
<cms type="article" category_code="tech">
  <list>
    <div class="article-card">
      {% if image_url %}<img src="{{ image_url }}" alt="{{ title }}">{% endif %}
      <h2><a href="{{ path }}">{{ title }}</a></h2>
      <div class="meta">{{ published_at_date }} | {{ author }}</div>
      <p>{{ abstract }}</p>
    </div>
  </list>
  <show>
    <article>
      {% if image_url %}<img src="{{ image_url }}" class="hero">{% endif %}
      <h1>{{ title }}</h1>
      <div class="meta">{{ published_at }} | {{ author }}</div>
      <div class="content">{{ content }}</div>
      <p><a href="{{ back_url }}">← Powrót do listy</a></p>
    </article>
  </show>
</cms>

Tworzenie artykułu przez API:

POST /cms/articles.json
{
  "article": {
    "title": "Nowy artykuł",
    "category_code": "tech",
    "abstract": "Krótkie streszczenie artykułu...",
    "summary": "Odpowiedź w 2-3 zdaniach - box \"w skrócie\" nad treścią.",
    "content": "Pełna treść artykułu...",
    "published_at": "2026-01-15T12:00:00Z",
    "site_id": 45
  }
}

Kompletny przykład

Poniżej przykład tworzenia prostej strony CMS przez API.

1. Utwórz Layout

POST /cms/layouts.jsonContent-Type: application/json

{
  "layout": {
    "code": "strona1-layout",
    "name": "Prosty szablon",
    "kind": "page",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <title>{{ name }} - {{ site.name }}</title>\n  <style>\n    body { font-family: sans-serif; max-width: 800px; margin: 40px auto; padding: 20px; }\n    header { background: #3b82f6; color: white; padding: 20px; margin-bottom: 20px; }\n    nav { margin-bottom: 20px; }\n    nav ul { display: flex; gap: 16px; list-style: none; padding: 0; }\n    nav a { color: #4b5563; text-decoration: none; }\n    nav a:hover { color: #3b82f6; }\n    nav a.active { font-weight: bold; color: #3b82f6; }\n    .paragraph { background: #f3f4f6; padding: 15px; margin: 10px 0; border-left: 4px solid #3b82f6; }\n  </style>\n</head>\n<body>\n  <header><h1>{{ site.name }}</h1></header>\n  <nav>\n    <ul>\n      {% for p in pages %}\n        <li><a href=\"{{ p.path }}\" {% if p.path == page.path %}class=\"active\"{% endif %}>{{ p.name }}</a></li>\n      {% endfor %}\n    </ul>\n  </nav>\n  {{ content }}\n</body>\n</html>"
  }
}

2. Utwórz Site

POST /cms/sites.jsonContent-Type: application/json

{
  "site": {
    "code": "strona1",
    "name": "Moja Strona",
    "kind": "www",
    "layout_code": "strona1-layout"
  }
}

3. Utwórz Stronę

POST /cms/pages.jsonContent-Type: application/json

{
  "page": {
    "code": "strona1-home",
    "name": "Strona Główna",
    "kind": "text",
    "path": "",
    "site_code": "strona1",
    "layout_code": "strona1-layout",
    "content": "<h2>{{ name }}</h2>\n\n<p>Witamy na naszej stronie!</p>\n\n{% for p in paragraphs %}\n  <div class=\"paragraph\">\n    <strong>{{ p.name }}</strong>\n    <p>{{ p.content }}</p>\n  </div>\n{% endfor %}"
  }
}

4. Utwórz Paragrafy (z podłączeniem do strony)

POST /cms/paragraphs.jsonContent-Type: application/json

{
  "paragraph": {
    "code": "strona1-intro",
    "name": "Wprowadzenie",
    "kind": "text",
    "content": "Witaj na mojej stronie! To jest przykładowa treść.",
    "site_code": "strona1",
    "page_code": "strona1-home",
    "priority": 10
  }
}
POST /cms/paragraphs.jsonContent-Type: application/json

{
  "paragraph": {
    "code": "strona1-about",
    "name": "O nas",
    "kind": "text",
    "content": "Jesteśmy firmą zajmującą się tworzeniem stron internetowych.",
    "site_code": "strona1",
    "page_code": "strona1-home",
    "priority": 5
  }
}

Paragraf z listą artykułów (kind articles — konfiguracja w fields, patrz sekcja “Szablony prezentacji paragrafów”):

POST /cms/paragraphs.jsonContent-Type: application/json

{
  "paragraph": {
    "code": "strona1-news",
    "name": "Aktualności",
    "kind": "articles",
    "site_code": "strona1",
    "page_code": "strona1-home",
    "box_code": "news",
    "system_template": "compact",
    "fields": { "category_codes": ["aktualnosci"], "per_page": 5 }
  }
}

5. Podgląd

Otwórz w przeglądarce:

/w/strona1

Aktualizacja przez code

Możesz aktualizować obiekty używając ich code zamiast id:

PATCH /cms/layouts/simple-layout.jsonContent-Type: application/json

{
  "layout": {
    "name": "Zaktualizowany szablon"
  }
}

Klonowanie Site

Możesz sklonować istniejący Site (stronę WWW) przez API. Przydatne gdy chcesz:

  • Utworzyć kopię roboczą do testowania zmian
  • Stworzyć nową wersję strony zachowując oryginał

Klonowanie podstawowe (tylko site)

POST /cms/sites.jsonContent-Type: application/json

{
  "from_site_code": "strona1",
  "site": {
    "name": "Strona testowa",
    "code": "strona1-test"
  }
}

Tworzy nowy Site z kopiowanymi ustawieniami (kind, layout_id, description, path_prefix, fields), ale bez layoutów, stron i paragrafów.

Klonowanie głębokie (deep clone)

POST /cms/sites.jsonContent-Type: application/json

{
  "from_site_code": "strona1",
  "deep_clone": "1",
  "site": {
    "name": "Strona testowa",
    "code": "strona1-test"
  }
}

Kopiuje site wraz ze wszystkimi powiązanymi elementami:

  • Layouty - kopie z nową nazwą zawierającą nazwę site w nawiasie, np. “Layout (Strona testowa)”
  • Strony (pages) - kopie z zachowaniem struktury parent-child i mapowaniem layout_id na nowe layouty
  • Paragrafy - kopie z mapowaniem layout_id na nowe layouty

Parametry

Parametr Typ Opis
from_site_code string Code źródłowego site’a
deep_clone string "1" aby skopiować layouty, strony i paragrafy

Przykład: workflow edycji przez LLM

  1. Sklonuj aktualną stronę:
    POST /cms/sites.json{
      "from_site_code": "produkcja",
      "deep_clone": "1",
      "site": { "name": "Wersja robocza", "code": "produkcja-dev" }
    }
    
  2. Wprowadź zmiany na kopii:
    PATCH /cms/pages/produkcja-dev-home.json{ "page": { "content": "... nowa treść ..." } }
    
  3. Podejrzyj zmiany:
    GET /w/produkcja-dev
    
  4. Po akceptacji - przenieś zmiany na produkcję lub usuń kopię

Wskazówki

  1. Placeholder w layoucie: Layout musi zawierać {{ content }} w miejscu gdzie ma być wstawiona treść strony
  2. Sortowanie paragrafów: Używaj priority (niższy = wyżej w boxie)
  3. Code vs ID: Używaj *_code dla czytelności i przenośności
  4. Preview bez auth: Endpoint /w/ nie wymaga autoryzacji
  5. Liquid syntax: Strony używają Liquid do dynamicznej treści
  6. Klonowanie do testów: Użyj deep_clone aby stworzyć kopię roboczą przed wprowadzaniem zmian
  7. Layout + site_id: Przy tworzeniu layoutu zawsze podawaj site_id — przypisuje layout do konkretnego site’a
  8. Markdown: Paragrafy i artykuły obsługują pole markup (html/markdown). Jeśli pole jest puste, format jest wykrywany automatycznie przy zapisie. Treść markdown jest renderowana jako GitHub Flavored Markdown (nagłówki, listy, tabele, bloki kodu, linki)
  9. NIGDY nie testuj zapisu na produkcji — nie wysyłaj testowych danych (np. "content": "test") do API, bo zmiany są natychmiast widoczne na produkcji. Jeśli chcesz sprawdzić czy API działa, użyj GET, nie PATCH/PUT z danymi testowymi
  10. Line endings \r\n — content z API używa \r\n (Windows line endings). Przy replace’ach w Pythonie używaj \r\n lub regex z re.DOTALL, nie \n
  11. Prezentacja paragrafów: zamiast stylować treść inline, wybierz gotowy szablon (kolumna system_template), layout (layout_id) albo napisz własny markup Liquid (kolumna template_content; dla articles bloki <list>/<show>) — szczegóły i przykłady w sekcji “Szablony prezentacji paragrafów”
  12. Tytuł paragrafu = pole name: text pokazuje go jako <h3> nad treścią (o ile paragraf nie ma layoutu ani szablonu prezentacji, który sam decyduje), articles jako <h2> nad listą. Nie dubluj tytułu w content

Powiązane

  • common_api — wspólne zasady API (format, autoryzacja, odpowiedzi)
  • cms_paragraphs — paragrafy: rodzaje, szablony prezentacji, zdjęcia, boxy

Czy ten wpis był pomocny?

Komentarze