[Intum Pomoc](https://intum.pl/pomoc.md) / [API](https://intum.pl/pomoc/cms/api.md)

# [Paragrafy stron i szablony prezentacji](https://intum.pl/pomoc/cms/api/paragrafy.md)

Kompletny opis paragrafów Intum CMS: rodzaje (`text`, `articles`), trzy poziomy szablonów prezentacji (wybrany szablon / layout / własny markup), załącznik-zdjęcie i zachowanie boxów. Endpointy i format API: [CMS API](https://intum.intum.pl/kb/intum-kb/cms/api/witryny-i-strony).

**Powiązane:** [CMS API](https://intum.intum.pl/kb/intum-kb/cms/api/witryny-i-strony) | [Wielojęzyczność](https://intum.intum.pl/kb/intum-kb/cms/api/wielojezycznosc) | [Wytyczne CMS](https://app.intum.pl/noe/prompt/cms_site_tips.md)

---

## Rodzaje paragrafów (`kind`)

### `text` — treść

- `content` w `html` lub `markdown` (pole `markup`, auto-detect gdy puste), renderowany przez Liquid (nieznane zmienne zostają w treści), markdown konwertowany do HTML (GFM).
- **Tytuł = pole `name`** (opcjonalne) — domyślnie `<h3>` nad treścią. Nie dubluj tytułu w `content`.
- **Zdjęcie = `fields.image_url`** (opcjonalne) — domyślnie `<img>` pod treścią. Publiczny URL — patrz "Załącznik (zdjęcie)".

### `articles` — lista artykułów

- Bez `content`. Konfiguracja w `fields`:
  - `category_codes` (array) — kody kategorii artykułów (`Cms::Category`, `/cms/categories.json`); artykuł wchodzi, gdy ma którąś z nich; puste = wszystkie,
  - `per_page` (integer) — limit; niepodany nie jest zapisywany, lista pokazuje 15.
- Tytuł = `name` → `<h2>` nad listą. Lista: artykuły opublikowane, najnowsze wg `published_at`, z witryny paragrafu/strony + wspólne.
- **Widok szczegółów**: gdy URL wskazuje artykuł (`/strona/artykuł`), pierwszy paragraf `articles` na stronie renderuje artykuł wariantem `show` zamiast listy (kolejne paragrafy `articles` pokazują normalnie listy). Strona z samym paragrafem, bez tagu `<cms type="article">`, ma więc działający widok szczegółów.

---

## Szablony prezentacji — trzy poziomy

Od najsłabszego do najsilniejszego (silniejszy nadpisuje słabszy):

Szablon żyje w kolumnach paragrafu (nazwy analogiczne do Cms::Layout): `system_template` (klucz gotowego szablonu), `template_content` (własny markup), `layout_id` (layout).

**1. Wybrany szablon — kolumna `system_template`:**

| Kind | Wartości | Uwagi |
|------|----------|-------|
| `text` | `framed` (ramka), `highlight` (wyróżnienie), `hero` (baner) | brak/nieznany = domyślna prezentacja: `<h3>` z `name` + treść + zdjęcie |
| `articles` | `default`, `cards` (z obrazkiem), `compact` (data + link) | każdy szablon ma warianty `list` (artykuł na liście) i `show` (szczegóły) |

Markupy gotowych szablonów w kodzie: `Cms::Paragraph::ParagraphRenderer::TEMPLATES` (+ `DEFAULT_TEMPLATE`), `Cms::Paragraph::ArticlesRenderer::TEMPLATES`.

**2. Layout paragrafu — `layout_id`** (Cms::Layout kind `paragraph`, wielokrotnego użytku):

- `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>` (ta sama konwencja co inner template `<cms type="article">`),
- formatka `/cms/layouts` przy kind `paragraph` ma przyciski wstawiające markup gotowych szablonów; w edytorze WYSIWYG layouty witryny są w tym samym selekcie co gotowe szablony.

Markup wybranego szablonu/layoutu można pobrać: `GET /cms/paragraphs/template.json?kind=text|articles&value=framed|layout:<id>` → `{"content": "..."}`.

**3. Własny markup — kolumna `template_content`** (odpowiednik przycisku "Dostosuj" w edytorze WYSIWYG):

- `text`: cały markup opakowania treści,
- `articles`: warianty w blokach `<list>...</list>` i `<show>...</show>` (markup bez bloków = sam wariant listy).

**Zmienne Liquid w szablonach:**

- `text`: `{{ content }}` (wyrenderowana treść), `{{ name }}`, `{{ description }}`, `{{ image_url }}` + własne pola z `fields`,
- `articles` (warianty `list`/`show`): `{{ id }}`, `{{ title }}`, `{{ path }}` (link do artykułu), `{{ author }}`, `{{ category_code }}`, `{{ abstract }}`, `{{ content }}`, `{{ image_url }}`, `{{ published_at }}`, `{{ published_at_date }}`, `{{ published_at_iso }}`, `{{ updated_at_iso }}`, `{{ back_url }}` (powrót na listę), `{{ fields }}` (pola artykułu).

**Normalizacja przy zapisie (obowiązuje przez API, CRUD i WYSIWYG):**

- nieznany `system_template` jest usuwany — renderuje się prezentacja domyślna,
- własny markup (`template_content`) pusty lub identyczny z bazowym jest usuwany (zamrożona kopia nie dostawałaby przyszłych poprawek szablonu); powrót do gotowego szablonu = wyślij `"template_content": null`,
- **błąd składni Liquid we własnym markupie blokuje zapis** (422, komunikat w `errors.template_content`),
- stare klucze `fields.template`/`fields.template_content` są przy zapisie przenoszone do kolumn, `fields.template_list`/`template_show` usuwane,
- `PATCH` z `fields` **podmienia cały jsonb** — modyfikując jeden klucz (np. `category_codes`), najpierw GET i odeślij pozostałe.

---

## Załącznik (zdjęcie) — `fields.image_url`

Plik żyje w `Cms::Asset` (kind `image`) na **publicznym S3/CDN**; paragraf trzyma sam URL. Przez API:

```
POST /cms/assets.json   (multipart: asset[file], asset[kind]=image, asset[site_id], asset[name]="paragraphs/<timestamp>-<nazwa>")
→ odpowiedź zawiera "s3_url" → wpisz do fields.image_url paragrafu
```

Formatka CRUD ma pole plikowe `paragraph[image]`, które robi to samo. Usunięcie zdjęcia = `fields.image_url: null` (asset zostaje). Domyślna prezentacja i gotowe szablony `text` renderują zdjęcie pod treścią.

---

## Boxy na stronie — `<cms type="box">`

| Atrybut | Działanie |
|---------|-----------|
| `id` | `box_code` paragrafów; sortowanie po `priority` rosnąco, przy równych po `id` |
| `max="N"` | limit paragrafów; pełny box nie pokazuje w trybie edycji przycisku dodania |
| `scope="site"` | box wspólny dla witryny (stopka): tylko paragrafy bez `page_id` i takie też zakłada; domyślnie box pokazuje paragrafy swojej strony + wspólne |

- Paragraf z `page_id` należy do strony; bez `page_id` jest wspólny (pokazuje się w swoim boxie na każdej stronie).
- Nowy paragraf bez jawnego `priority` trafia na koniec swojego boxa.
- Kolejność w boxie: `PATCH /cms/paragraphs/reorder.json` z `{"ids": [...]}` — zapisuje `priority` 1..N.

---

## Tytuł i rejestr zmian

- Tytuł paragrafu to zawsze pole `name` (nie `fields.title` — taki klucz jest czyszczony przy zapisie).
- Każde dodanie/edycja/usunięcie paragrafu trafia do rejestru zmian witryny (`Cms::ChangeLog`), o ile witryna ma włączoną flagę `change_log`. Wpis jest przypięty do strony paragrafu (`page_id`, ścieżka) — historia strony: `/cms/change_logs?page_id=X`.