Blokken en huisstijl
Twee harde regels bepalen hoe content en vormgeving uit elkaar blijven: een pagina is een geordende reeks blokken en huisstijl komt uit data. Deze pagina laat zien hoe dat in code is uitgewerkt.
Het blokmodel
Section titled “Het blokmodel”Een blocks-kolom (jsonb) bevat een array van blokobjecten:
[ { "id": "…", "type": "hero", "props": { "heading": "…", "actions": [] } }, { "id": "…", "type": "text", "props": { "body": "…" } }, { "id": "…", "type": "cardGrid", "props": { "items": [] } }]idis stabiel per blok-instantie, zodat een pagebuilder later kan herordenen zonder identiteit te verliezen.typeis de discriminator: hij bepaalt welk schema en welke renderer bij dit blok horen.propsis de inhoud en wordt gevalideerd tegen het Zod-schema van dat type.
Het TypeScript-type Block is een discriminated union in packages/contract/src/blocks.ts; blocksSchema valideert een hele array.
De zes bloktypen
Section titled “De zes bloktypen”| Type | Label in CMS | Inhoud |
|---|---|---|
hero |
Hero | Prominente pagina-intro met kop, optionele media en acties |
text |
Text | Markdown-tekst met optionele kop en breedte |
image |
Image | Eén afbeelding uit de mediabibliotheek met optioneel onderschrift |
cta |
Call to action | Gerichte oproep met één actie |
cardGrid |
Card grid | Raster van kaarten met titel, tekst, media en link |
quote |
Quote | Citaat met naam en optioneel portret |
Blokken met een kop (hero, cta, en text of cardGrid als ze een heading hebben) krijgen bij het renderen automatisch een oplopend kopniveau (h1, h2 en verder), zodat de pagina een correcte kopstructuur houdt zonder dat de redacteur daarover nadenkt.
Eén blok, drie onderdelen
Section titled “Eén blok, drie onderdelen”Een bloktype bestaat altijd uit drie dingen, en nooit één ervan los:
| Onderdeel | Waar | Wat |
|---|---|---|
| Schema | packages/contract/src/blocks.ts |
Zod-schema voor props, opgenomen in blockSchema |
| Renderer | packages/blocks/src/blocks/<Naam>.astro |
Astro-component die het blok als HTML tekent |
| Registry-entry | packages/blocks/src/registry-meta.ts en registry.ts |
Label en beschrijving voor het CMS, plus de koppeling van type naar component |
registry-meta.ts bevat bewust geen Astro-import: het CMS gebruikt het om formulieren en keuzelijsten op te bouwen (src/lib/cms/block-form.ts, default-block.ts, block-summary.ts) zonder renderers mee te laden. registry.ts voegt daar de componenten aan toe; BlockRenderer.astro is de enige plek die type aan een component koppelt.
Een nieuw blok toevoegen
Section titled “Een nieuw blok toevoegen”- Schema in
blocks.ts: eenz.objectmettype: z.literal("<naam>")en deprops, toevoegen aanblockSchemaen exporteren, inclusief het type. - Renderer
packages/blocks/src/blocks/<Naam>.astro. Gebruik uitsluitend theme-utilities (bg-primary,text-muted-foreground,rounded-md); geen kleuren, fonts of maten. Media viaMediaImage.astro. - Entry in
registry-meta.ts(type, schema, label, beschrijving) en inregistry.ts(component). TypeScript dwingt af dat elk type uit de union een entry heeft. - Controleer het CMS: het nieuwe type verschijnt in de blok-editor met een formulier afgeleid van het schema. Voeg waar nodig een standaardwaarde toe in
default-block.ts.
Draai daarna pnpm typecheck; de union en de satisfies-controle in de registry wijzen aan wat je vergeten bent.
Validatie bij schrijven én lezen
Section titled “Validatie bij schrijven én lezen”- Schrijven. De CMS-acties zetten formulierdata om in een blok en valideren met het schema uit het contract. Ongeldige blokken bereiken de database niet.
- Lezen.
parseBlocksForRender(packages/blocks/src/validate.ts) valideert bij elk render opnieuw, blok voor blok. Een onbekend type of een schemafout wordt een issue: in productie wordt zo’n blok overgeslagen en gelogd, in dev-modus toontUnknownBlock.astrowat er mis is. Eén kapot blok haalt dus nooit een hele pagina neer.
Waarom twee keer? Omdat schema’s evolueren. Een blok dat vorig jaar geldig was, kan na een schemawijziging ongeldig zijn; de leeskant vangt dat op.
Tekst in blokken
Section titled “Tekst in blokken”Het text-blok accepteert Markdown. packages/blocks/src/markdown.ts rendert met marked en haalt daarna met sanitize-html alles weg buiten een korte lijst veilige tags (alinea’s, nadruk, links, lijsten, citaten, code). Koppen zijn bewust niet toegestaan: de documentstructuur komt uit de blokkoppen. Links in navigatie en blokken gaan door safeHref uit het contract, dat alleen interne paden en http(s)-, mailto- en tel-links doorlaat.
De huisstijl: van jsonb naar CSS
Section titled “De huisstijl: van jsonb naar CSS”site.theme (jsonb) │ themeSchema.parse() ← packages/contract/src/theme.ts, elk veld met default ▼Theme-object { colors, typography, radii, spacing, logo } │ themeToCssVarsString() ← dezelfde file; de enige vertaallaag ▼<html style="--color-primary: …; --font-heading: …; --radius-md: …; --spacing-base: …"> │ @theme inline ← apps/site-template/src/styles/global.css ▼Tailwind-utilities: bg-primary, text-primary-foreground, font-heading, rounded-md, p-4 │ ▼Componenten en blokken gebruiken alleen die utilities- Elk veld heeft een default, zodat een nieuwe klant zonder ingestelde huisstijl toch een nette, werkende site heeft.
- Afgeleide
*Foreground-kleuren (primaryForegrounden zo verder) zijn verplicht, zodat een knop nooit een hardcodedtext-whitenodig heeft. - Waarden worden opgeschoond (
sanitizeCssVarValue) voordat ze in eenstyle-attribuut komen; een klant kan via de huisstijl geen CSS of scripts injecteren. --font-scalestuurt de root-fontsize,--spacing-basede basis van Tailwinds spacingschaal (p-4wordt vier keer de basis).- Lettertypes: een systeemstack, met optioneel een externe stylesheet-URL per font (
fontHeadingUrl,fontBodyUrl). Self-hosted fonts zijn een open punt. - Logo:
theme.logo.mediaIdverwijst naar eenmedia-rij; de header valt terug op de sitenaam.
De klant bewerkt dit alles onder Instellingen in het CMS (src/lib/cms/theme-form.ts). De toets uit de architectuur blijft de beste controle: verander de primaire kleur in de database, herlaad, en de site verandert mee.
Wat niet mag
Section titled “Wat niet mag”- Geen
bg-blue-600, geen hexcode, geen fontnaam in een component of blok van de klantsite. - Geen tweede vertaallaag naast
themeToCssVars(). - Geen paginatype met vaste contentvelden buiten het blokmodel.
Het master dashboard heeft geen tenant-theme; daar zijn gewone Tailwind-utilities juist correct.