Skip to content

Block Types

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
}

Alle blocks delen deze configuratie-opties:

PropTypeDefaultBeschrijving
spacing{ top: SpacingSize, bottom: SpacingSize }{ top: 'medium', bottom: 'medium' }Verticale ruimte
backgroundBackgroundConfig{ 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'

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’).

Header sectie met titel, subtitel en optionele buttons.

PropTypeBeschrijving
titlestringHoofdtitel (HTML)
subtitlestringOndertitel (HTML)
buttonsButton[]CTA buttons (max ~3 aanbevolen)
alignment'left' | 'center' | 'right'Tekst uitlijning
layoutstringLayout variant

Icoon: layout-template (Lucide)

Grid van features met iconen, titels en beschrijvingen.

PropTypeBeschrijving
itemsFeatureItem[]Array van features
columns1 | 2 | 3Grid kolommen

FeatureItem:

VeldTypeBeschrijving
iconstringLucide icoon naam
titlestringFeature titel
descriptionstringFeature beschrijving (HTML)

Icoon: grid-3x3

Call-to-action sectie met titel, beschrijving en buttons.

PropTypeBeschrijving
titlestringCTA titel (HTML)
descriptionstringCTA tekst (HTML)
buttonsButton[]Actie buttons
alignment'left' | 'center' | 'right'Uitlijning

Icoon: megaphone

Rich text content block.

PropTypeBeschrijving
contentstringHTML 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).

PropTypeBeschrijving
items{ items: FAQItem[] }Array van FAQ items (max 20)
buttonStyleButtonStyle?Block-level button stijl (solid/outline/transparent)
buttonColorstring?Button kleur (hex, legacy)
buttonColorTokenBrandColorToken?Button kleur token (primary/secondary/accent)

FAQItem:

VeldTypeBeschrijving
questionstringVraag (inline bewerkbaar via EditableText)
answerstringAntwoord (HTML, inline bewerkbaar via Tiptap RichText)
buttonButtonItem?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.

PropTypeBeschrijving
Alleen gemeenschappelijke props

Children: Ja — block.children bevat geneste Block objecten.

Icoon: layers-2

Twee-koloms layout.

PropTypeBeschrijving
columnRatiostringVerhouding (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.

PropTypeDefaultBeschrijving
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
imageImageBlockMediaundefined (placeholder dropzone)Media-object met url, alt, caption, focalPoint, blur_data_url
link{ href: string }undefinedOptionele 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:

  • smallcol-span-12 lg:col-span-6 lg:col-start-4 (~480px op 1024+)
  • normalcol-span-12 lg:col-span-8 lg:col-start-3 (~720px op 1024+)
  • largecol-span-12 (volledige gridContainer 960px)
  • fullscreenwidth: 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: MaybeSelectable rond <figure>, lege staat toont een dashed dropzone
  • aboveFold=truepriority (eager loading + fetchpriority high)
  • Caption rendert als <figcaption> onder de image, gecentreerd in textColors.subtext

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:

TypeWaardenBeschrijving
Basislight, darkVaste lichte/donkere kleur
AutoautoPast aan op achtergrondkleur parent block
Brandprimary, secondary, accentResolves naar var(--brand-{token})
Shadesprimary-50 t/m accent-900Shade 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.

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.

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 }
}
}

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; media veld wordt alleen getoond als mediaType !== 'none' (via visibleWhen).
  • rating = NumberStepper (alleen / + knoppen, step 0.5, range 0–5). Veld is alleen zichtbaar als block-level showRating aan staat (ook via visibleWhen met blockProps-context).
  • authorRole + authorCompany zijn klikbaar/inline-editable op canvas via EditableText keepEmpty — element blijft contentEditable bij lege value zodat de user kan blijven typen na alles te wissen.

Grid met klantbeoordelingen, 2 of 3 kolommen.

PropTypeDefaultBeschrijving
titleTitleConfig{ text: 'Wat klanten zeggen', tag: 'h2', size: 'lg' }Sectie-titel
subtitleSubtitleConfig{ 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
showRatingbooleanfalseToon sterrenrating
testimonials{ items: TestimonialItem[] }3 placeholder itemsMax 12 items

Icoon: layout-grid · Picker positie: na Features, voor CTA

Uitgelichte enkele quote, gecentreerd en groot weergegeven.

PropTypeDefaultBeschrijving
cardStyle'card' | 'plain' | 'bordered''plain'Kaart stijl
quoteMark'none' | 'classic' | 'modern''classic'Decoratief aanhalingsteken
showRatingbooleanfalseToon sterrenrating
testimonialTestimonialItem1 placeholder itemEnkel 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'.

Horizontale Embla carousel met testimonials.

PropTypeDefaultBeschrijving
titleTitleConfig{ text: 'Wat klanten zeggen', tag: 'h2', size: 'lg' }Sectie-titel
subtitleSubtitleConfig{ 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
showRatingbooleanfalseToon sterrenrating
testimonials{ items: TestimonialItem[] }4 placeholder itemsMax 12 items

Icoon: gallery-horizontal · Engine: Embla Carousel (zie ADR-009)

Alle testimonial blocks gebruiken packages/shared/src/testimonials/review-card.css voor card-styling, quote marks, ratings en carousel-layout. Belangrijke design-keuzes:

AspectGedrag
Card style cardAltijd 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 plainTransparent, geen border, padding 1rem 0.
Card style borderedBorder-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 footprintBeide 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 noneRendert echt niets (geen fallback decoratief teken in featured).
RatingTwee-laags overlay voor half-ster: grayscale baseline (★★★★★ opacity 0.2) + amber filled laag (width: (rating/5)*100%).
Grid + carousel kolom-gap1rem (matcht gridContainer gap-x-4 en grid-overlay debug).
Carousel pijltjesPosition 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 viewportoverflow: visible (niet hidden) zodat slides aan de rand niet hard worden afgekapt; section-overflow:hidden clipt op block-rand. Cursor grab / :active grabbing.
Carousel paginatieslidesToScroll: 1 + containScroll: 'trimSnaps' → next/prev gaat 1 slide door, dot-aantal = scrollSnapList().length (= unieke views, geen dode dots).
{
"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
}
]
}
}
}

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)

Gedefinieerd in apps/dashboard/src/lib/blocks.ts. Bevat per type:

  • type — identifier
  • label — weergavenaam (NL)
  • icon — Lucide icoon component
  • defaultData() — factory functie voor nieuw block met default props