Block Types
Overzicht
Section titled “Overzicht”Beam heeft 12 block types. Elk block heeft een type, unieke id (UUID), en props object. Blocks worden opgeslagen als JSONB array in de pages.blocks kolom.
interface Block { id: string // UUID type: BlockType // 'hero' | 'features' | 'image' | 'testimonials-grid' | 'featured-testimonial' | 'testimonials-carousel' | 'cta' | 'text' | 'faq' | 'group' | 'columns' props: BlockProps // Type-specifieke properties children?: Block[] // Alleen bij group en columns}Gemeenschappelijke Props
Section titled “Gemeenschappelijke Props”Alle blocks delen deze configuratie-opties:
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
spacing | { top: SpacingSize, bottom: SpacingSize } | { top: 'medium', bottom: 'medium' } | Verticale ruimte |
background | BackgroundConfig | { type: 'color', color: '#ffffff' } | Achtergrond |
corners | { top: boolean, bottom: boolean } | { top: false, bottom: false } | Afgeronde hoeken |
textColorMode | 'auto' | 'light' | 'dark' | 'auto' | Tekst kleurmodus |
SpacingSize: 'none' | 'small' | 'medium' | 'large'
BackgroundConfig
Section titled “BackgroundConfig”interface BackgroundConfig { type: 'color' | 'gradient' | 'image' | 'video' color?: string // Altijd bewaard als fallback colorToken?: BrandColorToken // 'primary' | 'secondary' | 'accent' — resolves to var(--brand-{token}) gradient?: { type: 'linear' | 'radial' angle?: number // Alleen bij linear position?: RadialPosition // Alleen bij radial stops: GradientStop[] // { color, position } } image?: { url: string media_id?: string // FK naar media tabel blur_data_url?: string // Base64 LQIP focalPoint?: { x: number, y: number } overlay?: { color: string, opacity: number } } video?: { url: string media_id?: string bunny_video_id?: string thumbnail_url?: string blur_data_url?: string focalPoint?: { x: number, y: number } overlay?: { color: string, opacity: number } }}cleanBackgroundConfig() stript inactieve velden voor opslag (bijv. gradient data als type = ‘color’).
Block Types
Section titled “Block Types”Header sectie met titel, subtitel en optionele buttons.
| Prop | Type | Beschrijving |
|---|---|---|
title | string | Hoofdtitel (HTML) |
subtitle | string | Ondertitel (HTML) |
buttons | Button[] | CTA buttons (max ~3 aanbevolen) |
alignment | 'left' | 'center' | 'right' | Tekst uitlijning |
layout | string | Layout variant |
Icoon: layout-template (Lucide)
features
Section titled “features”Grid van features met iconen, titels en beschrijvingen.
| Prop | Type | Beschrijving |
|---|---|---|
items | FeatureItem[] | Array van features |
columns | 1 | 2 | 3 | Grid kolommen |
FeatureItem:
| Veld | Type | Beschrijving |
|---|---|---|
icon | string | Lucide icoon naam |
title | string | Feature titel |
description | string | Feature beschrijving (HTML) |
Icoon: grid-3x3
Call-to-action sectie met titel, beschrijving en buttons.
| Prop | Type | Beschrijving |
|---|---|---|
title | string | CTA titel (HTML) |
description | string | CTA tekst (HTML) |
buttons | Button[] | Actie buttons |
alignment | 'left' | 'center' | 'right' | Uitlijning |
Icoon: megaphone
Rich text content block.
| Prop | Type | Beschrijving |
|---|---|---|
content | string | HTML content (sanitized via DOMPurify) |
alignment | 'left' | 'center' | 'right' | Tekst uitlijning |
Icoon: type
FAQ accordeon met vraag-antwoord paren. Inline editing op de canvas: vraag via EditableText, antwoord via inline Tiptap RichText (hover-to-edit met bubble menu + expand naar modal).
| Prop | Type | Beschrijving |
|---|---|---|
items | { items: FAQItem[] } | Array van FAQ items (max 20) |
buttonStyle | ButtonStyle? | Block-level button stijl (solid/outline/transparent) |
buttonColor | string? | Button kleur (hex, legacy) |
buttonColorToken | BrandColorToken? | Button kleur token (primary/secondary/accent) |
FAQItem:
| Veld | Type | Beschrijving |
|---|---|---|
question | string | Vraag (inline bewerkbaar via EditableText) |
answer | string | Antwoord (HTML, inline bewerkbaar via Tiptap RichText) |
button | ButtonItem? | Optionele button per item (bewerkbaar via EditableButton + ButtonEditModal) |
Icoon: circle-help
Container/wrapper block dat andere blocks kan bevatten. Gebruikt voor visuele groepering met gedeelde achtergrond/spacing.
| Prop | Type | Beschrijving |
|---|---|---|
| — | — | Alleen gemeenschappelijke props |
Children: Ja — block.children bevat geneste Block objecten.
Icoon: layers-2
columns
Section titled “columns”Twee-koloms layout.
| Prop | Type | Beschrijving |
|---|---|---|
columnRatio | string | Verhouding (bijv. '50-50', '33-67', '67-33') |
Children: Ja — twee child arrays (linker en rechter kolom).
Icoon: layout-grid
Solo afbeelding met instelbare breedte en hoogte. Caption en alt-tekst worden opgeslagen op het media-object zelf (binnen props.image), niet als aparte sidebar-velden.
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
width | 'small' | 'normal' | 'large' | 'fullscreen' | 'normal' | Kolom-span: small=6, normal=8, large=12, fullscreen=100vw viewport-bleed |
height | 'landscape' | 'portrait' | 'square' | 'original' | 'original' | Aspect ratio (matcht ColumnImageHeight); original = vrije hoogte, object-fit: contain |
image | ImageBlockMedia | undefined (placeholder dropzone) | Media-object met url, alt, caption, focalPoint, blur_data_url |
link | { href: string } | undefined | Optionele wrapper-link |
interface ImageBlockMedia { url: string media_id?: string blur_data_url?: string alt?: string // Bewerkbaar inline in image-picker caption?: string // Bewerkbaar inline; rendert als <figcaption> focalPoint?: { x: number; y: number }}Icoon: image · Picker positie: na Hero, voor Features.
Width-mapping:
small→col-span-12 lg:col-span-6 lg:col-start-4(~480px op 1024+)normal→col-span-12 lg:col-span-8 lg:col-start-3(~720px op 1024+)large→col-span-12(volledigegridContainer960px)fullscreen→width: 100vw; margin: calc(50% - 50vw)viewport-bleed buiten section padding
Render-gedrag:
- Astro renderer: 0 client-side JS, BlurImage met LQIP, optionele
<a>wrapper - React editor:
MaybeSelectablerond<figure>, lege staat toont een dashed dropzone aboveFold=true→priority(eager loading + fetchpriority high)- Caption rendert als
<figcaption>onder de image, gecentreerd intextColors.subtext
Button Schema
Section titled “Button Schema”Buttons komen voor in hero en cta blocks.
interface Button { id: string // UUID label: string // Button tekst href?: string // Externe URL page_id?: string // FK naar pages (interne link) style: 'primary' | 'secondary' | 'outline' | 'ghost' color?: string // DEPRECATED — hex fallback, bewaard voor backwards compat colorToken?: ColorToken // Primaire kleur definitie (zie tabel hieronder)}colorToken waarden:
| Type | Waarden | Beschrijving |
|---|---|---|
| Basis | light, dark | Vaste lichte/donkere kleur |
| Auto | auto | Past aan op achtergrondkleur parent block |
| Brand | primary, secondary, accent | Resolves naar var(--brand-{token}) |
| Shades | primary-50 t/m accent-900 | Shade varianten van brand kleuren |
Security: sanitizeColorToken() valideert tokens tegen een whitelist. Voorkomt CSS injection via kwaadaardige color waarden.
Migratie (058-059): Bestaande buttons met de oude hardcoded paarse kleur (#7c3aed) zijn gebackfilled naar colorToken: 'primary'. Het color veld is deprecated maar blijft als fallback voor buttons zonder colorToken.
Interne links: Als page_id is gezet, wordt de button href automatisch bijgewerkt bij slug wijziging via cascade-hrefs.
Pattern Containers
Section titled “Pattern Containers”Wanneer een pattern in een pagina wordt ingevoegd, wordt een speciaal group-block aangemaakt met pattern metadata:
{ id: string, type: 'group', props: { ... }, children: [...pattern blocks...], _pattern_id: string, // FK naar patterns tabel _pattern_name: string, // Pattern naam voor UI _is_synced: boolean // Of propagatie actief is}Detach: Bij detach worden _pattern_id, _pattern_name en _is_synced verwijderd. De blocks blijven, maar zijn niet meer gekoppeld aan het pattern.
JSONB Voorbeeld
Section titled “JSONB Voorbeeld”Een hero block zoals opgeslagen in pages.blocks:
{ "id": "b7f3a1e2-4d5c-6789-abcd-ef0123456789", "type": "hero", "props": { "title": "<h1>Welkom bij ons</h1>", "subtitle": "<p>Wij maken het verschil</p>", "buttons": [ { "id": "c8e4b2f3-5e6d-7890-bcde-f01234567890", "label": "Meer weten", "page_id": "a1b2c3d4-e5f6-7890-abcd-ef0123456789", "style": "primary" } ], "alignment": "center", "spacing": { "top": "large", "bottom": "medium" }, "background": { "type": "image", "color": "#1a1a2e", "image": { "url": "https://media.builtwithbeam.com/site-id/originals/img-id.jpg", "media_id": "d4e5f6a7-b8c9-0123-defg-h45678901234", "blur_data_url": "data:image/jpeg;base64,/9j/4AAQ...", "focalPoint": { "x": 0.5, "y": 0.3 }, "overlay": { "color": "#000000", "opacity": 0.4 } } }, "textColorMode": "light", "corners": { "top": false, "bottom": false } }}Testimonial Block Types
Section titled “Testimonial Block Types”Gedeeld Testimonial Item Schema
Section titled “Gedeeld Testimonial Item Schema”Alle drie testimonial block types gebruiken hetzelfde TestimonialItem schema:
interface TestimonialItem { id: string quote: string // Plaintext — geen HTML authorName: string authorRole?: string // Editable op canvas via EditableText keepEmpty authorCompany?: string // Editable op canvas via EditableText keepEmpty mediaType: 'avatar' | 'logo' | 'none' media?: { url: string media_id?: string blur_data_url?: string alt?: string } rating?: number // 0-5, step 0.5 (halve sterren ondersteund) // Default 5; 0 of undefined = niet tonen ondanks showRating}Editor-UX:
mediaType= select dropdown;mediaveld wordt alleen getoond alsmediaType !== 'none'(viavisibleWhen).rating= NumberStepper (alleen−/+knoppen, step0.5, range0–5). Veld is alleen zichtbaar als block-levelshowRatingaan staat (ook viavisibleWhenmet blockProps-context).authorRole+authorCompanyzijn klikbaar/inline-editable op canvas viaEditableText keepEmpty— element blijft contentEditable bij lege value zodat de user kan blijven typen na alles te wissen.
testimonials-grid
Section titled “testimonials-grid”Grid met klantbeoordelingen, 2 of 3 kolommen.
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
title | TitleConfig | { text: 'Wat klanten zeggen', tag: 'h2', size: 'lg' } | Sectie-titel |
subtitle | SubtitleConfig | { text: '' } | Optionele subtitel |
columns | '2' | '3' | '3' | Kolomaantal desktop |
mobileLayout | 'stacked' | 'carousel' | 'stacked' | Mobiele weergave |
cardStyle | 'card' | 'plain' | 'bordered' | 'card' | Kaart stijl |
quoteMark | 'none' | 'classic' | 'modern' | 'classic' | Decoratief aanhalingsteken |
showRating | boolean | false | Toon sterrenrating |
testimonials | { items: TestimonialItem[] } | 3 placeholder items | Max 12 items |
Icoon: layout-grid · Picker positie: na Features, voor CTA
featured-testimonial
Section titled “featured-testimonial”Uitgelichte enkele quote, gecentreerd en groot weergegeven.
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
cardStyle | 'card' | 'plain' | 'bordered' | 'plain' | Kaart stijl |
quoteMark | 'none' | 'classic' | 'modern' | 'classic' | Decoratief aanhalingsteken |
showRating | boolean | false | Toon sterrenrating |
testimonial | TestimonialItem | 1 placeholder item | Enkel object (geen array) |
Icoon: quote · Geen titel/subtitle — de quote is de blikvanger. Decoratief groot quote-teken als er geen media is en quoteMark = 'none'.
testimonials-carousel
Section titled “testimonials-carousel”Horizontale Embla carousel met testimonials.
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
title | TitleConfig | { text: 'Wat klanten zeggen', tag: 'h2', size: 'lg' } | Sectie-titel |
subtitle | SubtitleConfig | { text: '' } | Optionele subtitel |
slidesPerView | '1' | '2' | '3' | '2' | Zichtbare slides op desktop |
cardStyle | 'card' | 'plain' | 'bordered' | 'card' | Kaart stijl |
quoteMark | 'none' | 'classic' | 'modern' | 'classic' | Decoratief aanhalingsteken |
showRating | boolean | false | Toon sterrenrating |
testimonials | { items: TestimonialItem[] } | 4 placeholder items | Max 12 items |
Icoon: gallery-horizontal · Engine: Embla Carousel (zie ADR-009)
Gedeelde CSS
Section titled “Gedeelde CSS”Alle testimonial blocks gebruiken packages/shared/src/testimonials/review-card.css voor
card-styling, quote marks, ratings en carousel-layout. Belangrijke design-keuzes:
| Aspect | Gedrag |
|---|---|
Card style card | Altijd witte surface (#fff) + shadow + donkere tekst (#111827) ongeacht block-level light/dark text-mode. Tekstkleur binnen kaart wordt geforceerd via specificiteit (0,2,0) zodat Tailwind text-white op light-blocks de kaart-tekst niet onleesbaar maakt. |
Card style plain | Transparent, geen border, padding 1rem 0. |
Card style bordered | Border-color = currentColor (volgt block-tekstkleur automatisch). |
| Featured layout | .review-featured overschrijft de card-stijl padding NIET — surface-eigenschappen komen van de --card/--plain/--bordered class. Author-rij wordt verticale stack: avatar/logo, naam, rol/bedrijf gecentreerd. |
| Avatar/logo footprint | Beide 2.5rem × 2.5rem zodat meta-text op gelijke x-positie begint tussen items met avatar en items met logo. Logo gebruikt object-fit: contain binnen het vierkante slot. |
Quote-mark none | Rendert echt niets (geen fallback decoratief teken in featured). |
| Rating | Twee-laags overlay voor half-ster: grayscale baseline (★★★★★ opacity 0.2) + amber filled laag (width: (rating/5)*100%). |
| Grid + carousel kolom-gap | 1rem (matcht gridContainer gap-x-4 en grid-overlay debug). |
| Carousel pijltjes | Position absolute, left/right: -3.5rem (buiten viewport in section-padding), z-index: 10, witte solid surface met shadow. Vertical gecentreerd t.o.v. cards. Verborgen <md. |
| Carousel viewport | overflow: visible (niet hidden) zodat slides aan de rand niet hard worden afgekapt; section-overflow:hidden clipt op block-rand. Cursor grab / :active grabbing. |
| Carousel paginatie | slidesToScroll: 1 + containScroll: 'trimSnaps' → next/prev gaat 1 slide door, dot-aantal = scrollSnapList().length (= unieke views, geen dode dots). |
JSONB Voorbeeld (testimonials-grid)
Section titled “JSONB Voorbeeld (testimonials-grid)”{ "id": "a1b2c3d4-...", "type": "testimonials-grid", "props": { "spacing": { "top": "medium", "bottom": "medium" }, "background": { "type": "color", "color": "#f9fafb" }, "corners": { "top": false, "bottom": false }, "textColorMode": "auto", "title": { "text": "Wat klanten zeggen", "tag": "h2", "size": "lg" }, "subtitle": { "text": "" }, "columns": "3", "mobileLayout": "stacked", "cardStyle": "card", "quoteMark": "classic", "showRating": true, "testimonials": { "items": [ { "id": "uuid-...", "quote": "De beste tool die ik ooit heb gebruikt.", "authorName": "Jan de Vries", "authorRole": "CEO", "authorCompany": "Voorbeeld BV", "mediaType": "avatar", "media": { "url": "https://media.builtwithbeam.com/...", "alt": "Jan de Vries" }, "rating": 5 } ] } }}XSS Sanitisatie
Section titled “XSS Sanitisatie”Alle blocks met HTML content (text, hero, cta, features, faq) worden gesanitized via isomorphic-dompurify (@beam/shared/sanitize). Dit gebeurt in:
- Astro block renderers (publieke site) — server-side
- Dashboard block renderers — client-side (preview in editor)
Block Registry
Section titled “Block Registry”Gedefinieerd in apps/dashboard/src/lib/blocks.ts. Bevat per type:
type— identifierlabel— weergavenaam (NL)icon— Lucide icoon componentdefaultData()— factory functie voor nieuw block met default props