Tělo stránky z bloků
Tohle je ta druhá půlka návodu. Datová půlka končí u toho, že si vezmete objekt a vykreslíte ho po svém — patro o vaší šabloně neví nic. Tělo stránky složené z bloků je jiné: vykresluje ho patro, ne vy. Dostanete komponentu, které předáte data, a ona do vaší stránky vloží cizí značkování.
Kapitola je tedy hlavně o tom, jak si nad tím značkováním udržet kontrolu. Jsou na to dvě cesty a jdou po sobě: obarvit (stačí skoro vždycky) a nahradit (když má šablona ten blok postavený strukturálně jinak).
Čím se tělo vykreslí
Sekce “Čím se tělo vykreslí”---import PuckPage from "@patro/render/PuckPage.astro"import type { PuckData } from "@patro/render"
const entry = await client.getEntryBySlug(slug, locale)if (entry === null) { Astro.response.status = 404}---
<PuckPage data={(entry?.content ?? null) as PuckData | null} />PuckPage.astro se importuje přímo z cesty k souboru, ne z barelu
@patro/render — TypeScriptový modul .astro komponentu přeposlat neumí.
Stejně tak WidgetArea.astro, Menu.astro a EntryFields.astro.
Bere tři props a dvě z nich jsou nepovinné:
| Prop | Co to je |
|---|---|
data |
Tělo stránky z entry.content. null (čerstvá needitovaná stránka) vykreslí nic — 404 nebo náhradu řeší vaše routa. |
comments |
Vlákno diskuse. Bez něj se blok diskuse vynechá celý — popisuje to kapitola o oblastech a diskusi. |
blockOverrides |
Mapa „jméno bloku → vaše komponenta“. Bez ní se všechno vykreslí našimi bloky. |
Zúžení as PuckData není obcházení typu naslepo: entry.content je v kontraktu
svazek obou tvarů těla (formátovaný text i bloky) a do typu s tělem z bloků
richtext dokument nepustí zápisová strana. Které tělo se má vykreslit, říká
entry.type?.body — otazník tam patří, type je null u položky bez typu.
Tabulka tří druhů je v kapitole o položkách.
Smyčku první úrovně vlastní Astro
Sekce “Smyčku první úrovně vlastní Astro”Vypadá to jako jedno volání, ale uvnitř se obsah rozdělí na úseky: souvislé řady
statických bloků a mezi nimi bloky, které chtějí JavaScript (dnes jediný —
diskuse). Statické úseky jdou do Svelte rendereru naráz, blok s JavaScriptem
dostane client:visible, tedy „load and hydrate the component JavaScript once
the component has entered the user’s viewport“ (docs Astro,
reference/directives-reference.mdx → Client Directives).
Prakticky z toho plyne jedna věc, na kterou se dá narazit: oživuje se jen první úroveň. Blok vložený do „Sekce“ nebo „Sloupců“ se vykreslí staticky. U diskuse to redaktorovi nedovolí ani editor (blok do slotu nepustí), ale je dobré to vědět, než někdo začne hledat, proč mu formulář uvnitř sekce nereaguje.
Stránka bez diskuse zůstává úplně statická — nejde do ní ani řádek JavaScriptu, ani cizí skript.
Co si šablona musí přibrat
Sekce “Co si šablona musí přibrat”Tohle je ta daň, kterou úvod ohlašuje hned na začátku: bloky jsou Svelte
komponenty, takže i čistě Astro šablona potřebuje tři věci. Integrace spadne
nahlas: PuckPage.astro si Svelte renderer importuje napevno, takže bez ní
nemá sestavení čím .svelte soubor přečíst a build skončí chybou při parsování.
Zbylé dvě selžou tiše — ne chybou, ale rozbitým vzhledem.
1. Integrace Svelte
Sekce “1. Integrace Svelte”pnpm add @astrojs/svelte svelteimport { defineConfig } from "astro/config"import svelte from "@astrojs/svelte"
export default defineConfig({ integrations: [svelte()],})Balík @astrojs/svelte v aktuální řadě je pro Svelte 5; pro Svelte 3 a 4 se
instaluje @astrojs/svelte@5 (docs Astro,
guides/integrations-guide/svelte.mdx). K tomu si přidejte svelte.config.js —
docs ho chtějí proto, aby rozšíření Svelte v editoru umělo soubory správně
přečíst, a vitePreprocess v něm zapíná předzpracování (SCSS, Stylus):
import { vitePreprocess } from "@astrojs/svelte"
export default { preprocess: vitePreprocess() }2. Dva řádky @source
Sekce “2. Dva řádky @source”@import "tailwindcss";@import "@patro/blocks/tokens.css";
@source "../../node_modules/@patro/blocks/src";@source "../../node_modules/@patro/render/src";Bez nich se bloky vykreslí bez stylů a vypadá to jako rozbitý render. Důvod
je prozaický: bloky žijí v node_modules, a Tailwind 4 hledá třídy sám —
.gitignore přitom respektuje, takže závislosti přeskočí. Docs to mají popsané
přesně na tenhle případ (tailwind,
detecting-classes-in-source-files.mdx → „Explicitly registering sources“):
„This is especially useful when you need to scan an external library that is
built with Tailwind, since dependencies are usually listed in your .gitignore
file and ignored by Tailwind by default.“
Cesty jsou relativní k tomu CSS souboru, ne ke kořeni projektu.
3. Import slovníku rolí
Sekce “3. Import slovníku rolí”To je ten druhý @import výš. Bez něj nemají třídy uvnitř bloků krytí a Tailwind
pro ně nevygeneruje nic — o tom je celá další sekce.
Obarvení rolemi
Sekce “Obarvení rolemi”Bloky nenesou barvy. Nesou role: text-block-fg místo text-neutral-900,
bg-block-accent místo bg-blue-600. Hodnotu role určuje vaše šablona a je to
tenhle jeden blok CSS:
/* kdekoli za importem `@patro/blocks/tokens.css` */:root { --block-accent: oklch(58% 0.16 40); --block-accent-strong: oklch(50% 0.15 38);}Tím jsou odkazy, plná tlačítka a akcentové pruhy ve všech blocích terakotové. Do bloků se u toho nesahá, nic se nepřekrývá, nic se neforkuje.
Slovník rolí
Sekce “Slovník rolí”Přepisovat se má jen to, co má web opravdu jinak. Zbytek nechte na výchozích hodnotách — slovník opsaný celý jako kopie se od té naší rozejde při první změně.
| Role | K čemu je |
|---|---|
--block-fg |
Hlavní text — nadpisy, jména, tělo komentáře |
--block-fg-soft |
Souvislý text odstavců a druhotné popisky |
--block-fg-muted |
Doplněk čtený až po zbytku — popisek obrázku, čas |
--block-on-accent |
Text a obrys na akcentu |
--block-on-inverted |
Text a obrys na obrácené (tmavé) ploše |
--block-surface |
Podklad bloku, který sám nese obsah |
--block-surface-muted |
Jemně odlišená plocha — pruh hero, karta, štítek |
--block-surface-inverted |
Obrácená plocha — tmavý pruh výzvy |
--block-border |
Běžná hrana — rámeček karty, oddělovač |
--block-border-strong |
Silnější hrana pro pole formuláře |
--block-accent |
Akcent webu — odkazy, plná tlačítka |
--block-accent-strong |
Akcent tam, kde má být o stupeň důraznější |
--block-highlight |
Podbarvení zvýrazněného textu (<mark>) |
--block-danger |
Zamítnutí, chyba odeslání |
--block-success |
Potvrzení, přijatý komentář |
Rozvahu, proč jich je zrovna patnáct a který ze dvou sousedních odstínů u které
role vyhrál, má hlavička toho souboru, který si importujete — po instalaci ho
najdete v node_modules/@patro/blocks/src/tokens.css. Je to jediné místo, kde
se to dá dohledat.
Tři věci, které se u toho pletou
Sekce “Tři věci, které se u toho pletou”Předpona block- není kosmetika. Administrace přes shadcn už definuje
--color-accent, --color-border i --color-background a její accent je
téměř bílá neutrální barva, ne modrá. Nepředponovaný token by se v náhledu
editoru tiše chytil administrace.
Role bez tokenu nevydá NIC — ani špatnou barvu. Překlep ve třídě
(text-block-fq) projde vším: Svelte je řetězec, TypeScript o třídách neví, lint
taky ne. Tailwind pro neznámou třídu prostě nevygeneruje pravidlo a prvek zdědí
barvu rodiče, takže to vypadá skoro dobře. Totéž se stane, když se zapomene ten
@import slovníku. Když vám prvek v bloku vyjde v barvě okolí, hledejte
chybějící token, ne chybu v CSS.
Barva není všechno, co blok nese. Pruh výzvy má vedle role i zaoblení
a vnitřní rytmus (rounded-3xl px-6 py-10). Tokenizovat jde i to, ale zatím to
tokenizované není — takže když má vaše šablona hodně vlastní tvarosloví, je
odpověď o sekci níž.
Náhrada bloku komponentou
Sekce “Náhrada bloku komponentou”Když token nestačí, podstrčíte místo našeho bloku vlastní komponentu. Celé je to jedna mapa:
import type { BlockOverrides } from "@patro/blocks/overrides"import Hero from "../components/Hero.svelte"
export const BLOCK_OVERRIDES: BlockOverrides = { Hero }A pak ji předáte rendereru:
---import PuckPage from "@patro/render/PuckPage.astro"import { BLOCK_OVERRIDES } from "../lib/block-overrides"---
<PuckPage data={puckData} blockOverrides={BLOCK_OVERRIDES} />⚠️ Cesty jsou dvě a mapa musí do obou
Sekce “⚠️ Cesty jsou dvě a mapa musí do obou”Tělo stránky není jediné místo, kde se bloky kreslí — druhé jsou oblasti
widgetů (patička, postranní panel). Když mapu dostane jen PuckPage, budete
mít nahrazený blok v obsahu a náš v patičce. Je to nesrovnalost, kterou nechytne
typ ani žádný test — pozná se jedině prohlídkou hotového webu.
---// src/layouts/Base.astro — TÁŽ mapa, jinak se ty dvě plochy rozejdouimport WidgetArea from "@patro/render/WidgetArea.astro"import { BLOCK_OVERRIDES } from "../lib/block-overrides"---
<WidgetArea content={paticka} as="footer" blockOverrides={BLOCK_OVERRIDES} />Čtyři pravidla, která z toho plynou
Sekce “Čtyři pravidla, která z toho plynou”1. Komponenta musí být Svelte. Náhrada se uplatňuje uprostřed serverového
renderu Svelte a Astro komponenta tam nemůže: „Astro components are always
rendered to static HTML … you can only pass props that don’t do any HTML
rendering“ (docs Astro, guides/framework-components.mdx). Obal je ale laciný —
.svelte soubor s $props() a vaším značkováním, žádné runy nepotřebuje.
2. Klíč je jméno bloku z registru, takže překlep spadne na typu. Úplný seznam jmen je v tabulce na konci téhle kapitoly.
3. Props dostane náhrada tytéž, jaké by dostal náš blok. Prohnané ošetřením,
které zahodí hodnotu, na které by render spadl. Tvar si opište z naší komponenty
téhož jména, ne z toho, co vidíte v editoru — balík ji nese s sebou, takže ji
najdete v node_modules/@patro/blocks/src/svelte/ a props má každý blok
v typovaném $props(). Číst jde, importovat většinou ne: exports balíku vede
jen ke čtyřem komponentám a samostatný typ props nevydává vůbec.
4. Kontejner nahradit lze, ale musí si vykreslit svůj slot. „Sekce“ a „Sloupce“ nesou v props pole vnořených bloků; když je vaše komponenta nevykreslí, obsah uvnitř tiše zmizí. Od toho je veřejná podcesta:
<script lang="ts"> import BlockList from "@patro/blocks/BlockList.svelte" import type { PuckBlock } from "@patro/blocks"
let { items = [] }: { items?: readonly PuckBlock[] } = $props()</script>
<section class="moje-sekce"> <BlockList blocks={items} /></section>U „Sloupců“ je slotů víc a jmenují se podle počtu sloupců; jména si nemusíte
hádat, chodí z kořene balíku jako columnSlotNames("3").
Náhrady uvnitř vašeho kontejneru platí dál — mapa se dědí kontextem, ne propem, takže teče i skrz komponentu, která o ní neví.
Co nahradit nejde
Sekce “Co nahradit nejde”- Diskusi (blok
Comments). Kreslí ji Astro jmenným importem sclient:visible, ne renderer, takže by se náhrada tiše neuplatnila. Mapa ji proto mezi klíči nemá a pokus ji tam dát je typová chyba — což je ta lepší varianta než mlčení. - Blok, který dodal plugin. Jeho jméno má tvar
<slug>:<jméno>a do klíčů mapy nespadá. Blok pluginu patří tomu pluginu a zapíná se pro každý web zvlášť. - Podobu v editoru. Redaktor uvidí náš blok a výchozí paletu, ne vaši komponentu — administrace obsluhuje víc webů naráz. Je to jedna z mezí, které má návod sepsané na jednom místě.
Které bloky jde nahradit
Sekce “Které bloky jde nahradit”Jména jsou přesně ta, která patří do mapy. Sloupec vpravo je to, co vidí redaktor v paletě editoru — kdybyste ho potřebovali s někým sladit.
| Jméno v mapě | Blok v editoru |
|---|---|
Hero |
Hero |
Heading |
Nadpis |
Text |
Text |
Image |
Obrázek |
Video |
Video |
Button |
Tlačítko |
Spacer |
Mezera |
Faq |
FAQ |
RichText |
Formátovaný text (náhrada se netýká těla položky, viz Meze) |
Menu |
Menu |
LatestEntries |
Nejnovější stránky |
TermList |
Rubriky |
ArchiveList |
Archiv |
SearchBox |
Hledání |
Section |
Sekce (kontejner) |
Columns |
Sloupce (kontejner) |
Gallery |
Galerie |
Card |
Karta |
CallToAction |
Výzva k akci |
SocialLinks |
Odkazy na sítě |
Testimonials |
Posudky |
Diskuse (Comments) v tabulce schválně není — je to ten blok, který se
nenahrazuje.
Seznam se neopisuje ručně: hlídá ho test v packages/blocks, který ho porovná se
skutečným registrem na obě strany. Blok, který v patru přibude a v tabulce ne,
shodí testy dřív, než se tenhle návod stane neúplným.
Kudy dál
Sekce “Kudy dál”- Oblasti widgetů, diskuse a náhledy — patička a postranní panel, formulář komentářů a co dělat s výpadkem patra.
- Co návod slibuje a co ne — meze na jednom místě, ať se nemusí sbírat po kapitolách.