Položky, pole a výpisy
Stránka v patru se skládá ze tří nosičů obsahu a šablona s každým nakládá jinak:
- Titulek a slug — holé hodnoty.
- Typovaná pole (
data) — hodnoty vlastních polí typu obsahu, ke kterým chodí i jejich definice. - Tělo (
content) — buď formátovaný text, nebo tělo složené z bloků, nebo nic.
const entry = await client.getEntryBySlug(slug, locale)// {// id, slug, title,// data: { … }, // hodnoty vlastních polí// content: … , // tělo (podle `type.body`)// type: { name, slug, fields, body } | null,// terms: [{ … }], // kategorie a štítky// bylines: [{ … }], // podpisy autorů// }Typy obsahu a jejich pole
Sekce “Typy obsahu a jejich pole”Typ obsahu je šablona položky: „článek má perex, datum a obrázek“. Definice
chodí s každou stránkou (entry.type.fields), takže si šablona nemusí
schéma stahovat zvlášť ani ho znát dopředu.
entry.type je null u položky bez typu (starší obsah nebo smazaný typ). Není
to chyba — znamená to jen, že není podle čeho vypisovat pole.
Každá definice pole nese key (klíč v data), label (popisek), kind (druh),
required a dva přepínače: searchable (hodnota se přidá do fulltextového
indexu) a showOnPage (hodnota se vypíše na veřejné stránce — past hned
níž). Oba jsou volitelné a chybějící klíč u obou znamená „ne“.
Druhů polí je čtrnáct: text, textarea, number, boolean, date,
select, multiselect, url, slug, reference, image, file, repeater,
richtext.
⚠️ Pole se na webu zobrazí jen se zapnutým přepínačem
Sekce “⚠️ Pole se na webu zobrazí jen se zapnutým přepínačem”Tohle je past, kterou musí návod pojmenovat, protože se projeví jako „chybějící obsah“ a hledá se pak úplně jinde.
O tom, jestli hodnota pole vyjde na veřejnou stránku, rozhoduje showOnPage
na definici pole. Chybějící klíč znamená nezobrazovat.
Je to bezpečná strana omylu, ne omezení: pole, které mělo být vidět a není, si redaktor zapne jedním kliknutím v typu obsahu, kdežto hodnotu, která na web unikla, zpátky nikdo nevezme. Vzniklo to z konkrétní škody — dokud volba neexistovala, vypisovala se všechna pole typu, takže se návštěvníkovi naimportované stránky ukazovaly vnitřnosti cizího systému („Mec location id: 0“).
Prakticky: když v šabloně chybí hodnota, kterou redaktor vyplnil, nehledejte chybu v šabloně — podívejte se do typu obsahu, jestli má pole zapnuté zobrazení na stránce.
Přepínač se uplatňuje jen na první úrovni. Podpole opakující se skupiny
(repeater) se vypisují proto, že se zobrazuje jejich řádek; druhý přepínač
o úroveň níž by byl jen šum.
Vykreslení polí
Sekce “Vykreslení polí”Buď si je vykreslíte sami z entry.data a entry.type.fields, nebo použijete
hotovou komponentu:
---import EntryFields from "@patro/render/EntryFields.astro"---
{entry.type && entry.type.fields.length > 0 && ( <EntryFields fields={entry.type.fields} data={entry.data} mediaBaseUrl={PUBLIC_API_BASE} />)}Vykreslí <dl> popisek → hodnota jako statické HTML, nula JS. Filtr podle
showOnPage si dělá sama, takže se přes ni nic neveřejného neprotlačí.
mediaBaseUrl je adresa patra, ne vašeho webu — pole druhu image a file
z ní skládají absolutní adresu souboru. Kořenově relativní cesta by mířila na
váš web, kde soubor není. Prázdný výsledek nevykreslí ani prázdný <dl>.
Tři druhy těla
Sekce “Tři druhy těla”entry.type.body říká, co je v entry.content, a jsou to tři hodnoty:
body |
Co je v content |
Čím vykreslit |
|---|---|---|
"richtext" |
Dokument formátovaného textu | RichTextBody.astro |
"puck" |
Tělo složené z bloků | PuckPage.astro |
"none" |
Nic — typ tělo nemá | nevykreslovat |
---import RichTextBody from "@patro/render/RichTextBody.astro"import PuckPage from "@patro/render/PuckPage.astro"
const bodyKind = entry.type?.bodyconst isRichBody = bodyKind === "richtext"// Chybějící typ = starší obsah složený z bloků.const isPuckBody = bodyKind === undefined || bodyKind === "puck"---
{isRichBody ? <RichTextBody data={entry.content} /> : isPuckBody ? <PuckPage data={entry.content as PuckData | null} /> : null}entry.content je v kontraktu svazek obou tvarů těla, takže do PuckPage
jde se zúžením na PuckData. Není to obcházení typu naslepo: do typu s tělem
z bloků server richtext dokument nepustí, o to se stará zápisová strana.
"none" nesmí spadnout do větve s bloky. Kdyby ano, tělo uložené před
přepnutím typu na „bez těla“ by na webu svítilo dál — a redaktor by ho neměl jak
smazat, protože administrace mu ho už neukazuje.
HTML z richtextu nevzniká v patru, ale u vás. Patro pošle strom uzlů
a RichTextBody (přesněji renderRichText z @patro/render) z něj složí HTML
až během vykreslování vaší stránky, nad seznamem povolených prvků. Uložená
hodnota se tedy do stránky nikdy nedostane přímo — ale ta záruka stojí na tom
balíku, ne na serveru. Vlastní serializátor psaný „protože je to jen JSON“ je
přesně ta cesta, jak si do stránky pustit cizí značky.
Výpisy
Sekce “Výpisy”client.listEntries(filter) je jedna cesta pro všechny výpisové stránky —
index, výpis termu, výpis typu, archiv i stránky autora. Je to schválně: dvě
cesty by se dřív nebo později rozešly v tom, co je vlastně vidět.
// výpis kategorieawait client.listEntries({ locale, taxonomy: "kategorie", term: "aktuality", page })// výpis typuawait client.listEntries({ locale, type: "clanek", page })// archiv měsíceawait client.listEntries({ locale, year: 2026, month: 8, page })// stránky jednoho autoraawait client.listEntries({ locale, byline: "jan-novak", page })Položka výpisu (EntrySummary) nese id, slug, title, updatedAt
a terms — žádné tělo. Kategorie chodí rovnou v seznamu, takže se na ně
nemusíte doptávat po jedné.
Jak se pozná, že adresa je výpis
Sekce “Jak se pozná, že adresa je výpis”/kategorie/aktuality a /sluzby/uklid mají identický tvar. Rozhoduje mezi nimi
jedině slovník adres, takže se na něj musíte zeptat dřív, než víte, na co se
díváte:
const vocabulary = await client.getVocabulary()const [prvni] = cesta.split("/")const jeTaxonomie = vocabulary.taxonomies.some((t) => t.name === prvni)const jeTyp = vocabulary.types.some((t) => t.slug === prvni)Úsek adresy autorů a hledání ve slovníku není — ten chodí z konfigurace webu
(tenant.bylineRouteSlug, tenant.searchRouteSlug). Je to nastavení webu vedle
jazyků a pásma, ne obsah.
Stránkování
Sekce “Stránkování”Dávka má pevnou velikost (LISTING_PAGE_SIZE, dnes 20) a stránkuje se od 1.
Celkový počet u výpisů nechodí — server ho vědomě neposílá, byl by to
agregační dotaz navíc. „Je toho víc“ se pozná z toho, že dávka přišla plná:
---import { LISTING_PAGE_SIZE, pageFromParams, pagedHref,} from "@patro/shared"
const page = pageFromParams(Astro.url.searchParams)const entries = await client.listEntries({ locale, page })const maDalsi = entries.length === LISTING_PAGE_SIZE---
{maDalsi && <a href={pagedHref("/clanky", page + 1)}>Další</a>}Klíč v adrese je strana a neskládejte ho ručně. pagedHref ho přilepí přes
URLSearchParams, takže se neztratí dotaz už v adrese přítomný (?q=), a první
stránce ho nepřidá vůbec — jinak by týž výpis ležel na dvou adresách.
pageFromParams je druhá strana téhož a ošetří i nesmysl v adrese.
U hledání je to jinak a je to jediná výjimka: client.searchEntries total
vrací, takže se poslední stránka spočítat dá. Odsazení do dotazu se počítá
pageOffset(page).