Oblasti widgetů, diskuse a náhledy
Zbytek blokové půlky — tři věci, které mají tělo stránky společné (kreslí je tytéž bloky), ale zapojují se každá jinak.
Oblasti widgetů
Sekce “Oblasti widgetů”Patička, postranní panel a cokoli dalšího, co má na webu obsah spravovaný redaktorem a přitom to není stránka. Obsah oblasti je podstrom bloků, tedy totéž, co tělo stránky, jen bez kořene.
Jména oblastí vznikají u vás, a jenom u vás
Sekce “Jména oblastí vznikají u vás, a jenom u vás”import { defineWidgetAreas } from "@patro/render"import { getPatroClient } from "./patro"
export const widgetAreas = defineWidgetAreas({ client: getPatroClient, areas: [ { name: "paticka", label: "Patička", description: "Zobrazuje se úplně dole na všech stránkách webu.", }, { name: "postranni-panel", label: "Postranní panel", description: "Zobrazuje se vedle obsahu, vpravo.", }, ], onError: (error) => { console.error("patro: oblasti widgetů — ", error) }, onDeclared: (message) => { console.info(message) },})Tenhle seznam je jediné místo, kde se jména oblastí zadávají. Nikde jinde — ani v administraci. Patro se o nich dozví od vašeho webu a redaktorovi pak nabídne k vyplnění přesně tyhle a žádné jiné.
Je to obrana proti tiché díře, kterou má většina redakčních systémů: když jméno zadává člověk v administraci a podruhé ho píše autor šablony, tak se ta dvě jména dřív nebo později minou — a výsledek je, že redaktor vyplňuje patičku, kterou web nevykresluje, a nemá jak se to dozvědět.
label a description jsou pro toho, kdo oblast vyplňuje. Na webu se
nevykreslují, takže se píšou jako věta pro člověka, ne jako technický název.
Kdy se ohlašuje
Sekce “Kdy se ohlašuje”Při rozchodu, ne při startu. Web si oblasti tak jako tak čte (kreslí je) a v odpovědi vidí, co o nich patro ví. Když to sedí, neposílá se nic; když ne (nová oblast, přejmenovaný popisek, oblast, kterou patro vede jako osiřelou), odejde ohlášení — a na pozadí, takže na něj návštěvník nečeká.
Plyne z toho trojí: v ustáleném stavu se nezapisuje vůbec, po nasazení nové šablony se ohlásí samo a neúspěšné ohlášení se samo zopakuje.
Vykreslení
Sekce “Vykreslení”---import WidgetArea from "@patro/render/WidgetArea.astro"import { widgetAreas } from "../lib/widget-areas"import { BLOCK_OVERRIDES } from "../lib/block-overrides"
// JEDEN objekt kontextu na požadavek — viz níž, není to detail.const context = { locale, locals: Astro.locals }
const paticka = await widgetAreas.content("paticka", context)const panel = await widgetAreas.content("postranni-panel", context)---
<WidgetArea content={paticka} as="footer" label={widgetAreas.labelOf("paticka")} class="mt-16 border-t border-block-border px-6 py-12" blockOverrides={BLOCK_OVERRIDES}/>| Prop | Co to je |
|---|---|
content |
Obsah z widgetAreas.content(...). Prázdný nebo chybějící = nevykreslí se nic, ani obal. |
as |
Prvek obalu — div, footer, aside, section. Sémantika je věcí webu, ne oblasti. |
label |
Pojmenování pro čtečku. Chybějící údaj = žádný atribut, ne prázdný. |
class |
Vaše třídy na obal. |
blockOverrides |
Táž mapa jako do PuckPage — jinak se patička a tělo stránky rozejdou. |
Že se prázdná oblast nevykreslí ani obalem, je rozhodnutí, ne opomenutí: prázdný
<footer> by na stránce zabíral místo a vypadal jako rozbitý render. A stejně
tak se vědomě nerozlišuje „oblast je prázdná“ od „nepodařilo se dostat
k patru“ — pro vykreslení to znamená totéž a stránka na tom nesmí stát.
Jeden objekt kontextu na požadavek
Sekce “Jeden objekt kontextu na požadavek”Vyrobte si context jednou a předejte ho oběma voláním. Runtime si podle
toho objektu pamatuje, co už dotáhl, takže patička a postranní panel stojí jedno
kolo k patru, ne dvě. Kdo si kontext vyrobí pokaždé znovu, o nic nepřijde — jen
zaplatí to kolo navíc.
V locals je na Cloudflare ctx.waitUntil, kterým se obnovení odkládací paměti
a ohlašování odsune mimo cestu odpovědi návštěvníkovi.
Dvě věci, na které se dá narazit
Sekce “Dvě věci, na které se dá narazit”Jméno, které není v seznamu, vrátí prázdno a ohlásí se do onError. Typ ho
sice hlídá, ale .astro šablona i JavaScript ho obejdou — a tichý návrat prázdna
je přesně ten způsob, jakým oblast beze stopy mizí.
Jedna instance = jeden tenant. Odkládací paměť ani stav ohlašování nemají rozměr „pro který web“, protože se klient vyrábí z tokenu jednoho workeru. Kdo by chtěl jedním workerem obsloužit víc webů, musí vyrobit instanci na každý.
Diskuse
Sekce “Diskuse”Blok „Diskuse“ je jediný blok patra, který v prohlížeči potřebuje JavaScript. Zapojení má tři části: data, která umí dodat jen routa, tři adresy na vaší doméně a jedno pravidlo o tokenu.
Data dodává routa
Sekce “Data dodává routa”Blok neví, na které položce stojí — to ví jedině stránka, která ho vykresluje.
Vlákno se proto tahá v hlavičce routy a předá se PuckPage propem comments:
---import { env } from "cloudflare:workers"import PuckPage from "@patro/render/PuckPage.astro"import { commentsBlockSort, hasCommentsBlock, isOutage, loadContent, parseCommentNotice, type CommentsView,} from "@patro/render"
// Ptáme se JEN když blok diskuse na stránce doopravdy je — běžná stránka// neplatí ani kolo navíc.const wantsComments = entry !== null && hasCommentsBlock(puckData?.content ?? [])const notice = parseCommentNotice(Astro.url.searchParams)
const thread = wantsComments ? await loadContent( () => client.getComments(entry.id, { sort: commentsBlockSort(puckData?.content ?? []), }), patroDown, ) : null
// `loadContent` vrátí při výpadku značku, ne data — proto se z ní tady dělá// `null`. Bez toho by nedostupné patro shodilo celou stránku, ne jen diskusi.const threadOrNull = thread === null || isOutage(thread) ? null : thread
const comments: CommentsView | null = threadOrNull === null || entry === null ? null : { thread: threadOrNull, entryId: entry.id, endpoint: "/api/comments", reactionsEndpoint: "/api/comment-reactions", viewerEndpoint: "/api/my-reactions", text: t.comments, locale, timezone: tenant.timezone, // Veřejný klíč Turnstile z VAŠEHO prostředí; prázdný = widget se // nekreslí. Past kolem toho má vlastní sekci níž. turnstileSitekey: env.PUBLIC_TURNSTILE_SITEKEY ?? "", returnTo: `${Astro.url.pathname}${Astro.url.search}`, notice, }---
<PuckPage data={puckData} comments={comments} />text je katalog vět v jazyce stránky — patro posílá komentáře, ne jejich
opisky. Je to typ CommentsText z @patro/blocks (ne z @patro/render) a má
27 povinných klíčů: popisky formuláře, hlášky výsledku, slova pro počet
komentářů. Napsat se musí celý, ale jednou za jazyk — a co který klíč je,
nemusíte hádat: balík nese node_modules/@patro/blocks/src/comments-text.ts,
kde je u méně zřejmých klíčů komentář a hned pod typem prázdný katalog
EMPTY_TEXT se všemi 27 klíči jako kostra k opsání.
Prázdné adresy palců nejsou totéž:
- prázdný
reactionsEndpoint→ palce se kreslí jen jako čísla, kliknout nejde; - prázdný
viewerEndpoint→ přepínat jde, jen se zmáčknuté palce nezvýrazní (blok se na ně neptá).
Obojí je degradace, ne chyba.
Turnstile: klíč patří vám, tajný protějšek patru
Sekce “Turnstile: klíč patří vám, tajný protějšek patru”turnstileSitekey je veřejný klíč z prostředí vaší šablony
(u pilota PUBLIC_TURNSTILE_SITEKEY), ne hodnota z patra — objeví se stejně ve
zdroji stránky. Prázdný znamená, že se ochrana proti robotům nekreslí.
A tady je past, kvůli které to má vlastní nadpis: tajný protějšek drží patro a jen ono ověřuje token. Ty dvě strany se musí zapnout obě:
| stav | co se stane |
|---|---|
| klíč u vás, secret v patru | funguje |
| klíč u vás, secret v patru chybí | widget se kreslí, token nikdo neověří — neškodí, jen zdržuje |
| secret v patru, klíč u vás chybí | patro token vyžaduje a formulář ho nikdy nepošle → odmítnutý každý komentář |
Ten třetí řádek se pozná blbě: stránka vypadá v pořádku a odesílání tiše selhává
na všech komentářích. Veřejné API vám s tím nepomůže — jestli je secret
nastavený, se z client.getTenant() nedozvíte; ta informace je jen
v administraci (Nastavení webu → „Ochrana proti robotům“), a je to schválně, aby se stav
ochrany proti robotům nedal číst zvenku. Když tedy Turnstile zapínáte, zapněte
obě strany naráz a odešlete jeden zkušební komentář.
Nedostupné patro znamená stránku bez diskuse (blok se vynechá celý), ne
rozbitou stránku — táž priorita jako u navigace a oblastí. V ukázce výš to drží
loadContent plus isOutage; bez nich by se výpadek propsal do celé stránky.
Když nechcete kolo k patru na každé zobrazení
Sekce “Když nechcete kolo k patru na každé zobrazení”@patro/render má na vlákno komentářů hotovou odkládací paměť
(createCommentsCache) a zapojuje se na dvou místech naráz — jinak je
nekonzistentní a projeví se to tím, že autor po odeslání svůj vlastní komentář
nevidí:
// src/lib/comments.ts — jedna paměť na isolateimport { createCommentsCache } from "@patro/render"import { getPatroClient } from "./patro"
export const commentsCache = createCommentsCache({ // Klient se sem dává HODNOTOU, ne továrnou jako u navigace a oblastí // widgetů — proto ten mezikus: paměť vzniká jednou při načtení modulu, // kdežto `env` se smí číst až za požadavku. client: { getComments: (entryId, options) => getPatroClient().getComments(entryId, options), }, onError: (error) => { console.error("patro: vlákno komentářů — ", error) },})// stránka — `bypass` JEN u „schváleno"import { backgroundOf, parseCommentNotice } from "@patro/render"import { commentsCache } from "../lib/comments"
const notice = parseCommentNotice(Astro.url.searchParams)const thread = wantsComments ? await commentsCache.load( entry.id, { sort: commentsBlockSort(puckData?.content ?? []), bypass: notice === "approved" }, backgroundOf(Astro.locals), ) : null// src/pages/api/comments.ts — týž `createCommentProxy` jako v sekci// „Tři adresy na vaší doméně" níž, jen s jednou volbou navíc:createCommentProxy({ // … onAccepted: (entryId) => { commentsCache.invalidate(entryId) },})bypass patří jen k approved, ne ke každé hlášce v adrese. Vlákno se mění
výhradně tehdy, když komentář rovnou prošel; kdyby obcházelo paměť každé slovo,
je ?komentar=cokoli zadarmo dostupný obchvat celé paměti.
Tři adresy na vaší doméně
Sekce “Tři adresy na vaší doméně”Tohle je největší rozdíl proti redakčním systémům, které jsou jedna aplikace. Váš web běží na vlastní doméně a tenant token nesmí do prohlížeče — byl by čitelný ve zdroji stránky a kdokoli by s ním četl i psal do patra.
Formulář proto posílá na adresu u vás a teprve váš worker k tomu přidá token a přepošle to patru. Hotové prostředníky jsou v balíku:
import { env } from "cloudflare:workers"import { createCommentProxy } from "@patro/render"import type { APIRoute } from "astro"
export const POST: APIRoute = async ({ request }) => createCommentProxy({ baseUrl: env.PUBLIC_API_BASE, token: env.PATRO_TENANT_TOKEN, onError: (error) => { console.error("patro: odeslání komentáře — ", error) }, })(request)| Soubor | Metoda | Funkce |
|---|---|---|
src/pages/api/comments.ts |
POST |
createCommentProxy — odeslání komentáře |
src/pages/api/comment-reactions.ts |
POST |
createCommentReactionProxy — přepnutí palce |
src/pages/api/my-reactions.ts |
GET |
createViewerReactionsProxy — které palce má návštěvník zmáčknuté |
Adresy si volíte vy, ale musí sedět s tím, co dáváte do CommentsView — je to
řetězec na dvou místech a rozejití se projeví až čtyřistačtyřkou po odeslání.
Vyplatí se proto mít je jako konstantu.
Prostředník dělá čtyři věci, které z kódu nejsou vidět:
- Přidá IP návštěvníka. Přečte ji z
CF-Connecting-IP(Cloudflare ji dodává sám) a pošle patru vlastní hlavičkou — otisk se počítá až tam, protože sůl bydlí u patra. - Nepřepošle odpověď patra, jen ji přeloží na jedno strojové slovo. Chyby patra nesou české věty a váš web může být anglický; větu složí blok ze svého katalogu.
- Zvládne obojí druh těla. Oživený formulář posílá JSON, neoživený obyčejná
pole formuláře — a ten druhý nesmí skončit bílou stránkou s JSONem, takže se
na něj odpovídá přesměrováním zpátky s výsledkem v adrese (
303, aby obnovení stránky neodeslalo komentář podruhé). - Nepustí přesměrování mimo váš web. Adresu návratu píše prohlížeč, tedy kdokoli.
Ten výsledek v adrese je to, co ze stránky vyčte parseCommentNotice a předá
zpátky bloku jako notice. Bez toho by odeslání bez JavaScriptu skončilo tichým
návratem na článek a nikdo by nevěděl, jestli komentář někam došel.
Dvě pasti při zkoušení
Sekce “Dvě pasti při zkoušení”Přesměrování se vyhodnocují jen na GET a HEAD. Kdyby platila i na POST,
znamenalo by pravidlo se vzorem /api/* ztracené tělo formuláře — prohlížeč po
301 zopakuje požadavek GETem. Zápisové cesty tedy pravidly neprocházejí; ta
čtecí (my-reactions) ano, a nevadí to: v nejhorším zůstanou palce
nezvýrazněné.
POST s tělem formuláře chce hlavičku Origin. Astro má pro stránky
vykreslované za běhu zapnutou ochranu proti odeslání z cizího webu
(security.checkOrigin, od Astra 5 výchozí true — docs
reference/configuration-reference.mdx), takže požadavek bez Origin skončí
403. Prohlížeč tu hlavičku posílá vždycky, takže se to na funkci neprojeví —
ale při zkoušení curlem to vypadá jako chyba vašeho prostředníka a hledá se
úplně jinde.
Stránka s hláškou se nesmí ukládat
Sekce “Stránka s hláškou se nesmí ukládat”Když se návštěvník vrátí od odeslání, nese adresa větu „váš komentář čeká na schválení“. Ta patří jemu, ne komukoli dalšímu na téže adrese:
---if (notice !== null) { Astro.response.headers.set("cache-control", "no-store, private")}---Náhled rozepsané verze
Sekce “Náhled rozepsané verze”Redaktor si v administraci vygeneruje odkaz s ?preview=<token> a ten musí vaše
routa umět obsloužit. Token je podepsaný a nese v sobě položku, verzi i web,
takže se slug v adrese pro načtení obsahu vůbec nepoužije:
---const previewToken = Astro.url.searchParams.get("preview")const preview = previewToken === null ? null : await client.getPreview(previewToken)
// Nepublikovaný obsah na veřejné adrese — nesmí se nikde uložit.if (previewToken !== null) { Astro.response.headers.set("cache-control", "no-store, private")}---Volání je kvůli stručnosti bez obalu na výpadek — v ostrém kódu patří dovnitř
loadContent jako každé jiné volání patra (poslední sekce téhle stránky).
Tři věci, které k tomu patří:
- Neplatný, prošlý i cizí token vrací
null— udělejte z toho tutéž 404 jako z neexistujícího slugu. Ani tak se nedá poznat, že nějaký koncept existuje. - Vykreslujte náhled TOUŽ cestou jako publikovanou stránku. Jinak náhled přestane být náhledem — ukazoval by něco, co po publikaci vypadá jinak.
- Napište na stránku pruh, že je to náhled, a jako první věc. Odkaz se
přeposílá a bez pruhu si příjemce může myslet, že se dívá na to, co je venku.
Odpověď
getPreviewk tomu mástatuspoložky amatchesLive(dívám se na totéž, co je na webu?).
Výpadek patra
Sekce “Výpadek patra”Obsah si šablona tahá za běhu, takže nedostupné patro znamená stránku, kterou není čím naplnit. Balík na to má trojici, kterou stačí zapojit:
---import { isOutage, loadContent, outageResponse } from "@patro/render"
const patroDown = (error: unknown): void => { console.error("patro: obsah stránky — ", error)}const outage = (): Response => outageResponse()
const tenant = await loadContent(() => client.getTenant(), patroDown)if (isOutage(tenant)) return outage()
const entry = await loadContent( () => client.getEntryBySlug(slug, locale), patroDown,)if (isOutage(entry)) return outage()---outageResponse odpoví 503 s hlavičkou Retry-After a samonosnou stránkou
„web je chvíli mimo“. Pětistovka by znamenala „tahle adresa je rozbitá“, což je
pro vyhledávač po hodině na každé adrese webu signál k vyřazení z indexu; 503
znamená „dočasně, přijď později“.
Čtyři poznámky, každá zaplacená:
Obal patří jen kolem volání patra. Rozřešení adresy, sestavení odkazů a texty zůstávají venku — chyba ve vašem kódu se nemá vydávat za výpadek patra a schovat se za čitelnou hlášku. Ta patří na vaši chybovou stránku.
Značka výpadku je symbol, ne null. null je u čtení podle slugu legitimní
odpověď („tahle stránka není publikovaná“ → 404), takže by se výpadek nedal
odlišit a web by na nedostupné patro odpovídal čtyřistačtyřkou — tvrdil by, že
obsah neexistuje.
Vracejte bezargumentovou proměnnou, ne volání. astro check výraz vracený
z hlavičky stránky nekontroluje — chybný argument uvnitř return outageResponse({ … }) projde jako nula chyb, kdežto tatáž chyba o řádek výš
ohlášená je. Proto ta proměnná outage: v ní se volání zkontroluje a v návratu
se pak není čeho chytit. (Astro vracet Response z hlavičky stránky umí, je to
dokumentovaná vlastnost vykreslování za běhu — guides/on-demand-rendering.mdx
→ „Return a Response object“.)
Ozdoba se ošetřuje sama. Navigace, oblasti widgetů a dopočet výpisových bloků nedostupné patro tolerují — znamená pro ně stránku bez té ozdoby, ne 503. Tolerantní je tedy ozdoba, ne obsah.
Ta stránka je česky a přebít se nedá
Sekce “Ta stránka je česky a přebít se nedá”outageResponse bere jedinou volbu — retrySeconds. Text, jazyk ani značkování
se předat nedají: stránka nese <html lang="cs">, nadpis „Web je chvíli mimo“
a jednu větu pod ním. Plyne to z toho, kdy se posílá — jazyk webu drží patro
a to se v tu chvíli právě nedá přečíst, takže by se musel hádat z adresy.
Když váš web česky není, outageResponse nevolejte a vraťte si vlastní
Response. Nic vám v tom nebrání: isOutage je jen zúžení typu a na
outageResponse nijak nezávisí. Napodobte čtyři věci, kvůli kterým ten pomocník
existuje:
- stav
503, ne pětistovku (o důvod výš); - hlavičku
retry-after— výchozí číslo balík vyváží jakoOUTAGE_RETRY_SECONDS; - hlavičku
cache-control: no-store, jinak by výpadek přežil návrat patra; <meta name="robots" content="noindex">v HTML.
A napište ji samonosnou — vlastní <style>, žádný odkaz na sestavené CSS ani na
vaše společné rozvržení stránky. Obojí by spoléhalo na to, co se právě ukázalo
jako nedostupné: rozvržení si menu a oblasti vyzvedává z patra.
Kudy dál
Sekce “Kudy dál”- Tělo stránky z bloků — jak se bloky obarví a nahradí.
- Co návod slibuje a co ne — meze na jednom místě.