Skip to content

Block Types

Beam heeft 13 block types (waarvan 1 addon-gated). 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' | 'events-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 — markup gestript bij save + render (htmlToText)
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.
  • quote wordt via een multiline contentEditable bewerkt en slaat daardoor innerHTML op (<div>/<br> bij regelafbrekingen). cleanBlocksForSave normaliseert het op save naar platte tekst via htmlToText (Workers-safe, pure regex); alle render-sites draaien htmlToText óók defensief zodat legacy data met ingesloten markup geen letterlijke tags toont. .review-quote gebruikt white-space: pre-line zodat bewuste regelafbrekingen behouden blijven.

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 Swiper carousel met testimonials.

PropTypeDefaultBeschrijving
titleTitleConfig{ text: 'Wat klanten zeggen', tag: 'h2', size: 'lg' }Sectie-titel
subtitleSubtitleConfig{ text: '' }Optionele subtitel
cardStyle'card' | 'plain' | 'bordered''card'Kaart stijl
quoteMark'none' | 'classic' | 'modern''classic'Decoratief aanhalingsteken
showRatingbooleanfalseToon sterrenrating
testimonials{ items: TestimonialItem[] }4 placeholder itemsMax 12 items

Layout: vaste responsive opbouw, afgeleid van het standaard 12-koloms container-grid, onafhankelijk van block-instellingen — exact-fill zonder peek: 1 kaart op volle breedte (12 kolommen) mobiel, 2 gelijke kaarten van 6 kolommen vanaf 640px, 3 gelijke kaarten van 4 kolommen vanaf 768px via .review-carousel-slide--grid in review-card.css. Slide-breedte = (100% − (N−1)·--site-grid-gap) / N, wat exact een heel kolom-blok beslaat (a·C + (a−1)·G met a = 12/N kolommen); de slide-pitch is precies a kolom-pitches, dus elke snap blijft grid-aligned (kaart-randen op de kolomlijnen, bv. col 0/4/8/12 op desktop). De inter-card gap = --site-grid-gap (.review-carousel--grid .review-carousel-container { gap: var(--site-grid-gap) }, 0.5rem mobiel / 2.5rem ≥1024px). Met gelijke kaarten geldt de meetkundige wet “kies 2 van 3” (N zichtbaar · peek · randen-op-lijnen) — gekozen is randen-op-lijnen + N kaarten, dus géén peek; navigeren gaat per slide en blijft op elke snap uitgelijnd. Geen 100vw → de uitlijning klopt óók in de editor-canvas (canvas ≠ window). De slidesPerView-prop is deprecated: genegeerd bij render, alleen behouden voor backward-compat van bestaande block-data.

Icoon: gallery-horizontal · Engine: Swiper (zie ADR-010) · Carousel-opties: customCursor: false (klikbare knoppen) · cardsPerView: '3-op-een-rij' (--grid) · dots: true — uit het gedeelde optie-contract packages/shared/src/carousel.ts (zie ADR-011)

Horizontale Swiper carousel met de event-cards van de site. Alleen zichtbaar in de block-picker wanneer de events-addon actief is (BlockDefinition.requiredAddon = 'events').

PropTypeDefaultBeschrijving
titleTitleConfig{ text: 'Aankomende evenementen', tag: 'h2', size: 'lg' }Sectie-titel (inline-editable)
subtitleSubtitleConfig{ text: '' }Optionele subtitel (inline-editable, multiline)
limitnumber12Maximum aantal events om te tonen (RPC heeft eigen LIMIT 200)
includePastbooleanfalsePast events meenemen na de upcoming events

Icoon: gallery-horizontal · Engine: Swiper (slidesPerView: 'auto', spaceBetween: 0) · Slides: 1 op mobile, 2 op sm, 3 op lg via calc((100% - gap*(N-1)) / N)

Events worden server-side opgehaald via getPublishedEvents(siteId)niet in props opgeslagen. De carousel toont automatisch nieuwe events zonder dat de pagina opnieuw bewerkt of opgeslagen hoeft te worden. Cards-stramien identiek aan de archive page.

Toegankelijkheid: prev/next + dots + ArrowLeft/Right + Home/End keyboard nav. data-selectable="true" op carousel-wrapper + dots-controls voorkomt dat clicks bubbelen naar block-select.

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-gapGrid + niet-grid carousels (events-carousel, grid-block): 1rem op .review-carousel-container (matcht gridContainer gap-x-4 en grid-overlay debug). De testimonials exact-fill carousel (--grid) zet .review-carousel-container { gap: var(--site-grid-gap) } → de inter-card gap volgt exact het kolom-ritme (0.5rem mobiel / 2.5rem ≥1024px) zodat elke slide-pitch een heel aantal kolom-pitches is en de kaart-randen op de kolomlijnen vallen.
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 viewport (cardsPerView-modes)Drie modes uit het gedeelde optie-contract (packages/shared/src/carousel.ts, ADR-011). --grid ('3-op-een-rij', testimonials-carousel/events-carousel/related-events): overflow: hidden, 100%-relatief — kaarten vullen de container exact, randen op de gridlijnen, geen peek (klopt óók in editor-canvas). --bleed ('bleed', event-gallery): overflow: visible + section clipt op scherm-edge, rekent met 100vwalleen veilig op full-width publieke pagina’s, N kaarten + peek (niet grid-aligned). --contained/--1 ('1-up', grid-block single-card mobiel): overflow: hidden, één volle-breedte kaart. Cursor grab / :active grabbing.
Carousel paginatieSwiper slidesPerView: 'auto' → next/prev stapt 1 slide door (default step). Dot-aantal = aantal Swiper snap-posities (snapGrid); trailing snaps worden automatisch getrimd, dus geen dode dots. Editor en publiek delen dezelfde dot-logica (editor via useSwiperCarousel scrollSnaps, publiek via swiper.snapGrid).
{
"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