Přeskočit na obsah

Klient a jeho metody

createPatroClient vrátí objekt s dvaceti metodami (změřeno nad packages/render/src/client.ts). Tahle stránka je jejich úplný seznam, rozdělený podle toho, k čemu jsou — ne podle abecedy, protože k sobě patří jinak.

Než se do toho pustíte, dvě věci platné pro všechny:

  • Metoda buď vrátí hodnotu, nebo null (legitimní „to tu není“ / „beze změny“), nebo odmítne příslib (chyba). Rozvaha je v kapitole o napojení.
  • Kde je parametr locale volitelný, server bez něj dosadí výchozí jazyk webu.

Konfigurace webu — to, co potřebujete znát dřív, než se zeptáte na cokoli jiného: jazyky, výchozí jazyk, časové pásmo, úseky adres pro autory a hledání a identitu webu (název, popis, hotové adresy loga a ikony).

const tenant = await client.getTenant()
// {
// id, slug, name,
// description: string | null,
// logoUrl: string | null, iconUrl: string | null,
// locales: ["cs", "en"], defaultLocale: "cs",
// timezone: "Europe/Prague",
// bylineRouteSlug: "autor", searchRouteSlug: "hledani",
// hiddenBlocks: […],
// }

logoUrl a iconUrl jsou hotové adresy, ne identifikátory — z ID by je šablona složit neuměla. null znamená, že web tu věc nemá; nevymýšlejte za něj náhradu, prohlížeč si o ikonu řekne sám a dostane 404, což je normální stav webu bez ikony.

Slovník adres: jména taxonomií a slugy typů obsahu. Potřebujete ho dřív, než víte, na co se díváte/kategorie/aktuality a /sluzby/uklid mají identický tvar a rozhoduje mezi nimi jedině to, jestli je kategorie taxonomie tohohle webu.

const vocabulary = await client.getVocabulary()
// {
// taxonomies: [{ name: "kategorie", label: "Kategorie" }, …],
// types: [{ slug: "clanek", name: "Článek" }, …],
// }

Taxonomie se adresuje strojovým jménem, typ obsahu slugem — je to rozdíl, který se plete a v adrese se projeví. Popisek (label / name) je to, co patří do nadpisu výpisu.

Odlehčený seznam publikovaných stránek — bez těla, jen to, co potřebuje výpis. Bez zúžení je to celý index ve výchozím jazyce, první dávka. S filtrem je to výpis termu, typu, archivu nebo autora — jednou cestou, aby se ty stránky nemohly rozejít v tom, co je vlastně vidět.

const entries = await client.listEntries({
locale: "cs",
term: "aktuality", // slug termu
taxonomy: "kategorie", // zúží `term` na jednu taxonomii
type: "clanek", // slug typu obsahu
year: 2026, month: 8, // archiv (měsíc bez roku se ignoruje)
byline: "jan-novak", // stránky, pod kterými je podepsaný
page: 2, // stránkuje se od 1
})

Celkový počet nechodí — server ho vědomě neposílá, byl by to agregační dotaz navíc. „Je toho víc“ se pozná z plné dávky, viz stránkování.

getEntryBySlug(slug, locale?)

Sekce “getEntryBySlug(slug, locale?)”

Jedna publikovaná stránka i s tělem a rozřešeným typem (popisky a druhy polí).

const entry = await client.getEntryBySlug("o-nas", "cs")
if (entry === null) {
Astro.response.status = 404
}

null je jediná odpověď pro koncept, cizí slug i slug publikovaný jen v jiném jazyce. Server je vědomě neodlišuje — rozdíl by prozrazoval, co v administraci leží. Nedělejte tedy tichý fallback na jiný jazyk; tady vede poctivá 404.

Slug titulní stránky v daném jazyce, nebo null, když titulka nastavená není a kořen má ukázat výpis.

const front = await client.getFrontSlug(locale)

Vrací jen slug, ne obsah — a je to schválně. Kořen s ním udělá vnitřní přesměrování (Astro.rewrite) na detail, takže se titulka nemůže vykreslovat jinak než kterákoli jiná stránka.

Náhled nepublikované verze proti podepsanému tokenu. Ten přijde v adrese (?preview=…) a nese v sobě položku, verzi i web; klient ho jen přepošle.

const previewToken = Astro.url.searchParams.get("preview")
const preview =
previewToken === null ? null : await client.getPreview(previewToken)
// { entry, status: "draft" | "scheduled" | "published", matchesLive, scheduledAt }

Vrací stránku ve stejném tvaru jako getEntryBySlug, aby se náhled kreslil touž cestou a nemohl vypadat jinak než výsledek publikace. Navíc nese stav, ze kterého se skládá text výstražného pruhu.

Dvě věci k tomu patří a nejsou samozřejmé:

  1. Náhledovou odpověď nesmíte nechat uložit — je to nepublikovaný obsah na veřejné adrese. Nastavte cache-control: no-store, private.
  2. null je odpověď na jakoukoli vadu (chybějící, prošlý i pozměněný token, cizí web, smazaná položka) → táž 404 jako u neexistujícího slugu.

searchEntries({ q, locale?, limit?, offset? })

Sekce “searchEntries({ q, locale?, limit?, offset? })”

Fulltext v publikovaném obsahu. Hledá se nad živou verzí — koncepty ani naplánované stránky v indexu nejsou vůbec.

const page = await client.searchEntries({
q: dotaz,
locale,
limit: LISTING_PAGE_SIZE,
offset: pageOffset(cislo),
})
// { total: number, items: [...] }

Na rozdíl od výpisů tady total je, takže se dá spočítat poslední stránka. Prázdný dotaz se na server vůbec neposílá (vrátí prázdnou stránku) — je to odpověď na otázku, kterou nemá smysl klást.

⚠️ snippet NIKDY nevkládejte jako HTML

Sekce “⚠️ snippet NIKDY nevkládejte jako HTML”

Každý výsledek nese snippet — úryvek okolo shody. Zvýraznění v něm je značkami <mark>/</mark>, které kolem shody dopsala SQLite. Vypadá to jako připravené HTML a je to past: okolní text je syrový obsah stránky, tedy to, co napsal redaktor. set:html={hit.snippet} z toho udělá uloženou XSS.

Správně se úryvek rozseká podle těch dvou značek a text jde do stránky jako text — zvýraznění vznikne skutečným prvkem, ne vloženým řetězcem:

---
const parts = (snippet: string) =>
snippet
.split(/<mark>|<\/mark>/)
.map((text, index) => ({ text, hit: index % 2 === 1 }))
.filter((part) => part.text.length > 0)
---
<p>
{parts(hit.snippet).map((part) =>
part.hit ? <mark>{part.text}</mark> : <>{part.text}</>,
)}
</p>

<mark> a </mark> jsou jediné, co do úryvku přidává SQLite. Cokoli dalšího, co v něm vypadá jako značka, je obyčejný text od redaktora — a zůstane textem.

Menu, která web v daném jazyce má — jen jména a popisky, bez stromů.

const menus = await client.listMenus(locale)
// [{ name: "hlavni", label: "Hlavní menu", locale: "cs", itemCount: 7 }, …]

Je to obrana proti tiché díře: bez téhle metody by šablona musela hádat dohodnutý název menu a při neshodě by prostě nevykreslila nic. Prázdné pole = web menu nemá; není to chyba a není to null.

itemCount tam není pro ozdobu. Když si šablona vybírá menu sama, musí umět přeskočit prázdné — jinak navigace zmizí z celého webu jen proto, že prázdné menu má abecedně dřívější popisek.

Jedno menu jako rozřešený strom: každá položka má hotovou adresu a šablona si k ní nic nedopočítává. Položky, jejichž cíl na webu k vidění není, ze stromu vypadly už na serveru i s podstromem.

const menu = await client.getMenu("hlavni", locale)
// {
// name, label, locale,
// items: [{ id, label, href, target, titleAttr, cssClasses, children: [...] }, …],
// }

null = menu toho jména v tom jazyce neexistuje. Není to chyba: web, který menu ještě nemá založené, se má vykreslit bez navigace, ne spadnout.

getRedirects(knownVersion?)

Sekce “getRedirects(knownVersion?)”

Mapa pravidel přesměrování — přesné shody a vzory. Pravidla drží patro, vykonává je vaše šablona.

const map = await client.getRedirects(drzenaVerze)
if (map === null) {
// beze změny — nech si, co máš
}

Ručně to skoro nikdo volat nebude, protože celý mechanismus (paměť, vyhodnocení, hlášení) je hotový v createRedirectMiddleware — viz kapitola o routování.

Pošle nastřádaná hlášení zpátky do patra: nenalezené adresy a zásahy pravidel jednou zprávou.

await client.reportRedirects({ notFound: [...], hits: [...] })

Dávkově schválně — volá se z pozadí (waitUntil), ne v cestě odpovědi návštěvníkovi. Když selže, je to na vás: klient odmítne příslib a jestli se kvůli neodeslanému hlášení nemá kazit stránka, rozhodujete vy (hotový middleware rozhoduje, že ne).

Termy jedné taxonomie i s počty viditelných položek.

const terms = await client.getTerms("kategorie")
// [{ id, slug: "aktuality", label: "Aktuality", parentId: null, count: 12 }, …]

Seznam je plochý i u stromové taxonomie — zanoření se pozná z parentId a strom si z toho poskládá šablona, když ho chce.

Stránka výpisu termu z nich bere popisek do nadpisu — a hlavně z nich pozná, že term toho slugu vůbec existuje. null = taxonomie toho jména tu není (404); prázdné pole je něco jiného: taxonomie existuje a nemá termy.

listArchivePeriods(params?)

Sekce “listArchivePeriods(params?)”

Období, ve kterých web něco publikoval — roky, s months: true i jednotlivé měsíce.

const periods = await client.listArchivePeriods({
type: "clanek",
locale,
months: true,
})
// [{ year: 2026, month: 8, count: 4 }, …]

month: null je shrnutí celého roku, ne chybějící hodnota — bez months přijdou jen tyhle roční řádky. Prázdné pole = web v tom jazyce (a typu) nic publikovaného nemá; není to chyba.

Autoři webu v daném jazyce, seřazení podle jména, a k nim definice jejich vlastních polí.

const { bylines, fields } = await client.listBylines(locale)

Definice chodí i tady, ne jen u detailu, a je to nutnost: fieldValues je holá mapa klíč → hodnota a bez popisků a druhů by ji nebylo čím vykreslit. Profil, který v tom jazyce neexistuje, v seznamu není.

getByline(slug, locale?)

Sekce “getByline(slug, locale?)”

Jeden profil i s definicemi polí — týž tvar jako type.fields vedle data u stránky, takže to zvládne tatáž komponenta.

const profile = await client.getByline("jan-novak", locale)
if (profile === null) {
Astro.response.status = 404
}

null = autor toho slugu v tom jazyce tu není. Udělejte z toho 404, ne prázdný profil.

Tělo stránky, oblasti a diskuse

Sekce “Tělo stránky, oblasti a diskuse”

Tahle pětice patří k blokové půlce. Metody jsou tu popsané kvůli úplnosti; zapojení má vlastní kapitolu.

resolveListingData(requests, locale?)

Sekce “resolveListingData(requests, locale?)”

Dopočet výpisových bloků — na seznam požadavků vrátí seznam hotových výpisů (odkazy, strom menu, adresa hledání). Redaktor si do těla stránky vloží blok „výpis článků“ a tohle je to, čím se naplní.

const data = await client.resolveListingData(pozadavky, locale)

Odpověď se nesmí držet v paměti. Je to jediná část obsahu, která je čerstvá právě proto, že se na ni ptáme pokaždé: publikace nové stránky by otiskem verze oblastí nehnula, takže by výpis zamrzl. Prázdný seznam požadavků se na server neposílá vůbec, takže stránka bez výpisového bloku neplatí ani kolo navíc.

listWidgetAreas(locale?, knownVersion?)

Sekce “listWidgetAreas(locale?, knownVersion?)”

Oblasti widgetů, které má web v daném jazyce vykreslit — jméno, popisek, popis a obsah, všechno jednou odpovědí.

const areas = await client.listWidgetAreas(locale, drzenaVerze)
if (areas === null) {
// beze změny
}

getWidgetArea(name, locale?)

Sekce “getWidgetArea(name, locale?)”

Jedna oblast — pro web, který si celý seznam nedrží.

const paticka = await client.getWidgetArea("paticka", locale)

null = oblast toho jména tu není, nebo ji web přestal kreslit (osiřela). Ty dva případy server vědomě neodlišuje a pro toho, kdo se ptá, znamenají totéž: nevykreslovat.

declareWidgetAreas(declaration)

Sekce “declareWidgetAreas(declaration)”

Ohlásí patru, které oblasti šablona kreslí — celý seznam, ne přírůstek, takže co v něm není, patro označí za osiřelé.

const result = await client.declareWidgetAreas({
areas: [{ name: "paticka", label: "Patička", description: "" }],
})
// čísla o tom, co se PRÁVĚ ZMĚNILO — „nic nového" je řada nul

Tohle je ten mechanismus, kvůli kterému administrace nabídne redaktorovi k vyplnění přesně ty oblasti, které šablona opravdu kreslí, a žádné jiné. Ručně ho volat nemusíte — defineWidgetAreas z @patro/render ho zavolá sám, a jen při rozchodu.

getComments(entryId, options?)

Sekce “getComments(entryId, options?)”

Schválené vlákno pod položkou — celý strom o jedné úrovni naráz.

const thread = await client.getComments(entry.id, {
sort: "best", // Wilsonova mez; jinak pořadí vzniku
knownVersion: drzena,
})

Plochá (nevláknová) podoba čtení tudy vědomě nevede.

Podmíněné dotazy (knownVersion)

Sekce “Podmíněné dotazy (knownVersion)”

Tři metody berou otisk podoby, kterou si držíte — getRedirects, listWidgetAreas, getComments. Pošle se jako If-None-Match, a když se od té verze nic nezměnilo, server odpoví 304 a klient vrátí null = nech si, co máš.

Je to ten častý případ, ne okrajový: patička se kreslí na každé stránce, takže se web ptá pořád dokola, ale mění se jen při uložení obsahu.

A je to důvod, proč null u nich neznamená chybu ani prázdno. Prázdná mapa pravidel je objekt s prázdnými poli, prázdná diskuse objekt s prázdným polem. null je třetí stav a plete se to nejsnáz ze všeho na téhle stránce.

Odkládací paměť si nemusíte psát — @patro/render má hotovou pro pravidla (createRedirectMapCache), oblasti (createWidgetAreasCache), navigaci (createMainMenuCache) i diskusi (createCommentsCache).