Přeskočit na obsah

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.

Terminál
pnpm add @astrojs/svelte svelte
astro.config.mjs
import { 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):

svelte.config.js
import { vitePreprocess } from "@astrojs/svelte"
export default { preprocess: vitePreprocess() }
src/styles/global.css
@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.

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.

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:

src/lib/block-overrides.ts
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 rozejdou
import 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:

src/components/Sekce.svelte
<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í.

  • Diskusi (blok Comments). Kreslí ji Astro jmenným importem s client: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.