Skip to content

Functionaliteit

De editor is een fullscreen canvas (EditorLayout) zonder sidebar navigatie. Blocks worden verticaal gestapeld en inline bewerkt.

┌──────────────────────────────────────────────────────┐
│ Toolbar: [← Terug] [Titel] [Undo/Redo] [Publiceren] │
├────────────────────┬─────────────────────────────────┤
│ │ │
│ Block Picker │ Canvas │
│ (sidebar, │ (blocks verticaal) │
│ toggle) │ │
│ │ ┌─────────────────┐ │
│ ─── OF ─── │ │ Geselecteerd │ │
│ │ │ block (inline │ │
│ Block Settings │ │ editing) │ │
│ (sidebar, │ └─────────────────┘ │
│ per block) │ │
│ │ │
├────────────────────┴─────────────────────────────────┤
│ Status bar: [Block count] [Last saved] [Draft/Pub] │
└──────────────────────────────────────────────────────┘

De sidebar wisselt tussen twee modi:

  • Block picker — lijst van beschikbare block types om toe te voegen
  • Block settings — configuratie van het geselecteerde block (props, achtergrond, spacing)

Tekst en content worden direct op de canvas bewerkt:

ComponentGebruikInteractie
EditableTextTitels, subtitels, FAQ vragenSingle-click = inline edit, Enter = blur (single-line) of <br> (multiline)
RichText (Tiptap)Hero/text content, FAQ antwoordenHover = ring, 120ms = editor mount, bubble menu bij selectie, expand naar modal
EditableButtonListButtons in alle blocksKlik = ButtonEditModal, block-level style+kleur via Design tab

Gedrag bij verlaten van een veld:

  • Klik buiten op een ander block → veld blurt, block wordt NIET geselecteerd (eerste klik = alleen exit)
  • Selectie-ring verdwijnt wanneer een inline editor gefocust is
  • RichText stript lege paragrafen aan begin/eind bij blur

De Merk pagina bevat een “Verhaal” tab waar gebruikers merkgegevens invullen die de AI versterken. Data wordt opgeslagen in brand_profiles.onboarding_data als JSONB (BrandStory interface).

5 card-secties:

  1. Over je bedrijf — Naam, slogan, beschrijving, branche, opgericht
  2. Doelgroep & positionering — Doelgroep, B2B/B2C, regio, differentiator, primaire + secundaire CTA
  3. Missie & waarden — Missie, visie, kernwaarden (tag-input, max 6)
  4. Tone of voice — Aanspreekvorm (je/u/jullie), 3 sliders (formeel, serieus, technisch), tone beschrijving, verboden woorden (tag-input, max 10)
  5. Contactgegevens — Email, telefoon, adres

Volledig veldoverzicht: zie Data Architectuur — brand_profiles.

De editor toolbar bevat een AI-knop (sparkle icoon) die een modal opent voor het genereren van hero content. Beschikbaar in zowel de page editor als de pattern editor.

Twee modi:

  1. Hero varianten (POST /ai/generate-hero) — genereert 3 varianten van titel + subtitel. Via AI Test knop in de editor toolbar.
  2. Per-block generatie (POST /ai/generate-block) — genereert content voor elk individueel block. Via ✨ knop in de block hover-toolbar.

Per-block generatie (primaire feature):

  • ✨ Sparkle knop verschijnt bij hover over elk block in de editor
  • Popover met tekstveld: beschrijf wat je wilt → Enter om te genereren
  • Smart merge: AI wijzigt alleen content (teksten, labels, items). Styling (kleuren, achtergrond, spacing, button styles) blijft behouden
  • Verfijnen vs vervangen: AI bepaalt automatisch of het de bestaande content aanpast (“maak de titel korter”) of volledig vervangt (“maak hier een FAQ over verzendkosten”)
  • Block context: AI kent de layout-instellingen (kolombreedte, achtergrond type, aantal items) en past de tekst lengte hierop aan
  • Bestaande content: Huidige teksten worden meegestuurd zodat de AI kan verfijnen in plaats van vervangen

Technisch:

  • Model: Configureerbaar via AI_MODEL env var (default: Claude Sonnet)
  • Context: Volledige brand story + mood preset + block instellingen + bestaande content
  • Output: Anthropic tool_use voor gegarandeerd JSON + Zod output validatie per block type
  • Security: Prompt injection preventie, IDOR check, 25s timeout, rate limit 10 req/min
  • Enrichment: Buttons krijgen automatisch id, color, colorToken. Features/FAQ items krijgen id.

Volledige technische beschrijving: zie AI Architectuur.

De volledige AI pagina-generatie is beschreven in AI Content Generatie.

11 block types beschikbaar. Volledige schema’s en props in Block Types.

TypeIcoonDoel
herolayout-templateHeader sectie met titel, subtitel, buttons
featuresgrid-3x3Grid van features (1-3 kolommen)
testimonials-gridlayout-gridGrid van klantbeoordelingen (2 of 3 kol.)
featured-testimonialquoteUitgelichte enkele testimonial
testimonials-carouselgallery-horizontalHorizontale Embla carousel met testimonials
ctamegaphoneCall-to-action sectie
texttypeRich text content (HTML)
faqhelp-circleFAQ accordeon
grouplayers-2Container/wrapper voor geneste blocks
columnslayout-grid2-koloms layout
buttonssquareButton groep
  • Klik op een block om te selecteren — opent block settings sidebar
  • Multi-select via Ctrl/Cmd+klik — voor bulk operaties
  • Drag & drop via @dnd-kit — herordenen binnen de pagina
  • Dupliceren — kopieert block met nieuwe ID
  • Verwijderen — met bevestiging als block children bevat

Het hero block gebruikt een Tiptap WYSIWYG-editor (gebouwd op ProseMirror) voor titel en subtitel in plaats van plain text contentEditable. Andere blocks (CTA, Features, FAQ) gebruiken nog EditableText.

Bubble Menu — Zwevende toolbar bij tekst selectie op het canvas:

  • Inline formatting: vet, cursief, onderstrepen, doorhalen, link
  • Block type: paragraaf, H1, H2, H3, H4
  • Text styles: Lead tekst, Accent, Klein (CSS class-based)
  • Expand knop: opent de tekst in een uitklapbare modal voor gedetailleerd bewerken

Expand Modal — Uitklapbare editor via sidebar-knop of bubble menu:

  • Single editor instance — dezelfde Tiptap editor verplaatst tussen canvas en modal (geen synchronisatie nodig)
  • FixedToolbar in modal — statische toolbar boven editor (geen bubble menu in modal)
  • ModalHeader met titel “Tekst bewerken” + sluitknop
  • Deferred store writes — bewerkingen in modal worden pas naar store geschreven bij sluiten
  • Static HTML tijdens animaties — canvas en modal tonen HTML snapshot terwijl open/close animaties spelen; EditorContent mount/unmount buiten animatieframes
  • Frozen height — canvas behoudt hoogte wanneer editor naar modal verplaatst

Tekst Kleur Auto-detectie:

  • textColorMode: 'auto' berekent lichte of donkere tekst op basis van achtergrond
  • Kleuren: isLightColor() analyse op hex kleur of gradient stops
  • Afbeelding/video: default naar lichte tekst; alleen donkere tekst bij lichte overlay met >30% opacity
  • Brand color tokens: resolven naar werkelijke kleurwaarde voor nauwkeurige detectie

Architectuur:

  • Tiptap history uitgeschakeld — Zustand store history (undo/redo) is de single source of truth
  • rAF-debounced store updates bij typen (deferred via requestAnimationFrame)
  • Echo prevention — per-editor write timestamps (Map) voorkomen dat editors hun eigen store-updates teruglezen
  • state.page bail-out — Zustand subscription skipt pad-traversal als state.page referentie ongewijzigd
  • Zustand selectors — individuele action selectors voorkomen onnodige re-renders
  • Lazy-loaded via React.lazy() → apart vendor-editor Vite chunk (~135KB gzipped)
  • Link bewerken via LinkEditModal (zelfde UI pattern als ButtonEditModal)
  • Backwards compatibel: plain text wordt automatisch in <p> tags gewrapped via normalizeToHtml()
  • Publieke site rendert HTML direct via set:html (geen sanitize op Cloudflare Workers — content is pre-sanitized in dashboard)
  • Max 50 snapshots in de history stack
  • Debounce: 500ms window. Prop-wijzigingen (tekst typen, kleur kiezen, spacing aanpassen) worden samengevoegd tot 1 history entry
  • Directe snapshot: Structurele wijzigingen (block toevoegen, verwijderen, verplaatsen, pattern detach) slaan onmiddellijk op — geen debounce
  • Keyboard shortcuts: Ctrl/Cmd+Z (undo), Ctrl/Cmd+Shift+Z (redo)
  • Bij bereiken van het limiet (50): oudste snapshots worden verwijderd

Pagina’s worden niet automatisch opgeslagen. De gebruiker klikt expliciet op “Opslaan” (Ctrl/Cmd+S).

Dirty state detectie: Vergelijking tussen page (huidige state) en originalPage (snapshot bij laden). De “Opslaan” button is alleen actief bij wijzigingen.

Save flow:

Gebruiker klikt "Opslaan"
→ cleanBlocksForSave() stript inactieve background velden
→ PUT /pages/:id (Supabase RLS check)
→ Bij succes: originalPage = huidige page (dirty state reset)
→ Fire-and-forget cache purge
→ Toast: "Pagina opgeslagen"

Elk block kan een achtergrond hebben. Volledige TypeScript schema in Block Types — BackgroundConfig.

TypeBeschrijving
KleurEnkele kleur (hex). Default: wit
GradientLineair of radiaal. Meerdere stops met kleur + positie
AfbeeldingUit media library. Focal point picker, overlay opacity
VideoAchtergrond video. Thumbnail + blur placeholder

De kleur wordt altijd bewaard als fallback, ook bij gradient/image/video. textColorMode: 'auto' berekent automatisch lichte of donkere tekst op basis van de achtergrond (zie Rich Text Editing — Tekst Kleur Auto-detectie).

Color Picker: De achtergrond-tab bevat een volledige color picker met:

  • Kleur / Gradient tabs — segmented control om te wisselen tussen solid kleur en gradient mode
  • Saturation/hue picker — visuele kleurkiezer met drag-pointers (motion.div spring-animatie)
  • Gradient stop editor — horizontale balk met draggable stops (2-5 stops, lineair of radiaal)
  • Lineair/Radiaal toggle — gradient type met angle-input (lineair) of 9-positie selector (radiaal)
  • Brand kleuren — merkkleuren als presets (solid mode: achtergrondkleur, gradient mode: actieve stop-kleur)
  • Brand presets — merkgradients als volledige gradient presets (alleen in gradient mode)
  • Active state — actieve brand kleur/preset krijgt een visuele ring-indicator (border-selection)
  • Opacity/alpha slider alleen beschikbaar in gradient mode, niet bij solid kleuren
  • Gradient defaults gebruiken brand primary + secondary kleuren (geen hardcoded waarden)
  • ColorPicker is lazy-loaded (React.lazy) — pas geladen bij openen van de achtergrond-tab

Brand color tokens: Achtergronden en buttons kunnen gekoppeld worden aan merkkleur tokens (primary, secondary, accent). De kleur resolves naar var(--brand-{token}) — bij wijziging van het kleurenschema updaten alle gekoppelde elementen automatisch mee.

Buttons gebruiken colorToken in plaats van hardcoded hex kleuren. Dit koppelt buttons aan het brand kleurensysteem.

Token typeWaardenGedrag
Basislight, darkVaste lichte/donkere kleur
AutoautoPast aan op achtergrondkleur van het parent block
Brandprimary, secondary, accentBrand kleur token via var(--brand-{token})
Shadesprimary-100, accent-700, etc.Shade varianten van brand kleuren

Migratie: Bestaande buttons met de oude hardcoded paarse kleur zijn via database migratie (058-059) gebackfilled naar colorToken: 'primary'. Het color veld (hex) is deprecated maar blijft als fallback.

Security: sanitizeColorToken() valideert input tegen een whitelist van toegestane tokens. CSS injection via kwaadaardige color waarden is hiermee geblokkeerd. Zie Data Architectuur — ButtonItem voor het schema.

Patterns kunnen als container in een pagina worden ingevoegd:

Block picker → "Patterns" tab → Selecteer pattern
→ Group block aangemaakt met pattern blocks als children
→ _pattern_id, _pattern_name, _is_synced metadata
→ Bij synced (`is_synced: true` in patterns tabel): "Update all uses" propagatie beschikbaar
→ Detach: verbreekt koppeling, blocks blijven
  • Nieuw: /editor/new — lege pagina met default titel en gegenereerde slug
  • Bewerken: /editor/:id — laadt bestaande pagina in editor
  • Lijst: /pages — overzicht met titel, slug, status, block count, datum
  • Formaat: ^[a-z0-9]([a-z0-9-]{1,61}[a-z0-9])?$ (max 63 tekens)
  • Reserved slugs: app, admin, www, api, staging, dashboard, login, register, etc.
  • Validatie: Client-side (direct feedback) + API-side (dubbele check)
  • Slug wijziging: Triggert cascade-hrefs — alle interne links op andere pagina’s worden automatisch bijgewerkt
StatusZichtbaarheidRLS
draftAlleen teamleden in dashboardTeam members (authenticated)
publishedPubliek via Astro siteIedereen (geen auth nodig)

Publiceren = status wijzigen naar published + cache purge. Depubliceren = terug naar draft.

VeldBeschrijvingFallback
meta_titlePaginatitel voor zoekmachinestitle
meta_descriptionBeschrijving in zoekresultaten
og_imageSocial media previewSite-level og_image_url
no_indexVerberg voor zoekmachinesblock_search_engines (site-level)
hide_headerVerberg header navigatiefalse
hide_footerVerberg footer navigatiefalse
Bestand selecteren
→ Client berekent SHA-256 hash
→ Check duplicaat (GET /media/check-duplicate)
→ Duplicaat? → Hergebruik bestaand item (toast melding)
→ Nieuw? → Upload naar R2
→ Blur placeholder genereren (20px LQIP)
→ Thumbnail genereren (400px WebP)
→ DB record aanmaken
→ Toast: "Bestand geüpload"

Limieten: 10 MB per bestand (images/docs), 100 MB video’s. 500 MB totaal per site. Zie Bedrijfslogica — Quota’s.

Ingebouwde zoekfunctie voor Unsplash en Pexels:

Media library → "Stock" tab → Zoekterm invoeren
→ Provider kiezen (Unsplash / Pexels)
→ 20 resultaten per pagina
→ Selecteer foto → Opgeslagen als externe media
→ Fotograaf attributie bewaard

Zie Unsplash & Pexels integratie voor API details.

URL invoeren → SSRF check → HEAD probe (type, grootte) → DB record. Externe media wordt niet gekopieerd naar R2 — de originele URL wordt gebruikt.

  • Tags per site, case-insensitive (LOWER comparison)
  • Bulk operaties: meerdere media items selecteren → tag toewijzen/verwijderen
  • Filteren en zoeken op tags in media library

count_media_references(media_id) telt hoeveel pagina’s en patterns een media item gebruiken. Dit wordt getoond in de media detail view en gebruikt als waarschuwing bij verwijdering.

  • Twee menu types: header en footer (selecteerbaar in site settings)
  • Menu’s zijn site-scoped (site_id kolom) — elke site heeft eigen menu’s
  • Hierarchische structuur (parent-child tree)
  • Drag & drop herordenen via positie-veld
VeldTypeBeschrijving
labeltextWeergavenaam in menu
page_idUUIDInterne link → pagina slug
urltextExterne URL (als geen page_id)
parent_idUUIDParent item (voor submenu’s)
positionintegerVolgorde

Bij interne links wordt de URL automatisch afgeleid van de pagina slug.

De header pagina (/menus/header) combineert drie configuratiesecties:

Navigatie type — twee layouts:

TypeLayout
logo-menu-buttonLogo (links) — Menu (midden) — CTA Button (rechts)
menu-logo-menuMenu 1 (links) — Logo (midden) — Menu 2 (rechts)

Menu & Button configuratie:

  • logo-menu-button: menu selectie + CTA button (tekst, URL, stijl, kleur via ButtonEditModal)
  • menu-logo-menu: linker menu + rechter menu selectie

Header Design — visuele configuratie met live preview:

  • Achtergrond: brand kleuren, blur effect, transparant bij hero
  • Border & schaduw: aan/uit, kleur, subtiel/medium
  • Positie: sticky/static/fixed, hoogte (compact/default/tall), verberg bij scrollen
  • Typografie: menu items, logo tekst, CTA button (font-size, weight, letter-spacing)
  • Logo selectie: kies uit logo varianten geupload bij Brand → Elementen

Live preview — sticky browser mockup naast de opties:

  • Toont header + homepage blocks (above-the-fold)
  • Reageert direct op wijzigingen (geen opslaan nodig)
  • Klik om te vergroten naar full-width (Motion layoutId morph)
  • Scroll gedrag zichtbaar: hide-on-scroll, transparant bij hero

Logo’s worden beheerd op de Brand pagina onder het Elementen tab:

  • Meerdere logo varianten: Primary, Wit, Donker, Icoon, of custom
  • Opgeslagen als logos JSONB array op brand_profiles
  • In de header configuratie selecteer je welk logo wordt getoond (selectedLogoId)
  • Backwards compat: eerste logo = logo_url (legacy veld)

Team management is site-scoped: elk team is gekoppeld aan een specifieke site, niet aan een owner. Een gebruiker kan editor zijn op site A en viewer op site B. De team_members tabel gebruikt site_id als primaire scope — owner_id is verwijderd.

Bij het aanmaken van een nieuwe site voegt de add_owner_to_site_team() trigger de eigenaar automatisch toe als team member met role owner.

  • Overzicht: /settings/users — lijst van teamleden met naam, email, rol, datum
  • Rol wijzigen: Owner/admin kan rollen aanpassen (dropdown)
  • Verwijderen: Owner/admin kan leden verwijderen (met bevestiging)
  • Verlaten: Non-owners kunnen zelf het team verlaten
  • Uitnodigen: Voornaam, achternaam, email, rol selecteren
  • Validatie: Geen self-invite, geen duplicaat, alleen owner/admin
  • Email: HTML template via Resend met magic link
  • Status tracking: pending → accepted / declined / revoked / expired
  • Resend: Uitnodiging opnieuw versturen als deze verlopen of niet ontvangen is

Zie Bedrijfslogica — Rollen & Rechten voor de volledige matrix.

Configureerbaar via /brand — de Styling Builder voor de visuele identiteit van een site.

TabBeschrijving
Presets6 sfeer-presets als startpunt (Warm & Aards, Bold & Expressief, etc.)
Kleuren7 kleuren: primair, secundair, accent, neutraal, achtergrond + tekst donker/licht. Preset paletten + vrije kleurkiezer met inline WCAG contrast badge per kleur
Typografie12 font-combinaties + fijnafstelling: heading dikte/letterafstand/regelhoogte, body dikte/regelhoogte, typeschaal (compact/standaard/dramatisch)
ElementenHoekafronding (5 opties), schaduw (4 opties)
ExportCSS custom properties, W3C Design Tokens JSON, Tailwind v4 @theme — kopieer naar klembord of open in browser

Live preview: Split layout met rechts een voorbeeldkaart die direct reageert op wijzigingen. Toont heading, body, buttons, card, links en kleurenpalet.

Design token features:

  • 7 brand kleuren — Primary, secondary, accent, neutral, surface + textDark/textLight. Neutral en surface worden gebruikt voor semantische tokens (borders, muted text, surface backgrounds)
  • WCAG contrast badges — Client-side contrast checking per kleur via checkWcagContrast(). Badge toont ratio + pass/fail status, met suggestie voor accessible alternatieven
  • Semantic tokens — Automatisch afgeleide tokens (surface, border, on-primary, danger/success/warning/info) als CSS variabelen
  • OKLCH shade scale — 10-staps kleurschaal (50-900) per brand kleur gegenereerd via OKLCH kleurruimte
  • Dark mode — Automatische dark mode CSS via @media (prefers-color-scheme: dark) met OKLCH-berekende overrides
  • Typography fijnafstelling — Heading weight (100-900), letter-spacing (-0.05em tot 0.05em), line-height (0.8-1.5). Body weight (300-700), line-height (1.2-2.0). Type scale selector (compact/standaard/dramatisch)
  • Token versioning — Automatische snapshots bij elke wijziging + handmatige named snapshots met restore via versiegeschiedenis modal
  • Export — 3 formaten: CSS Custom Properties, W3C Design Tokens JSON, Tailwind v4 @theme
  • Accessibility — WAI-ARIA tabs pattern, aria-labels op alle controls, live regions voor status updates, keyboard navigatie

Data: brand_profiles tabel (1 per site) met design_tokens JSONB en mood_preset referentie. token_versions tabel voor versiegeschiedenis (max 50 auto-snapshots).

Gebruikers kunnen meerdere sites aanmaken en beheren (max 10 per owner).

In het user dropdown menu staat een site switcher met favicon en sitenaam. Bij wisselen worden alle dashboard data (pages, menus, settings, domains) herladen voor de geselecteerde site.

Via de “Nieuwe site” knop in de site switcher:

  • Naam invoeren — slug wordt automatisch gegenereerd (lowercase, spaties → hyphens)
  • Bootstrap maakt automatisch een site_settings en brand_profile record aan
  • Quota check: max 10 actieve sites per owner (check_site_quota() trigger)

Soft delete met 14 dagen hersteltijd:

  1. Owner klikt “Verwijder site” in settings → bevestiging via modal
  2. DELETE /sites/:id zet deleted_at timestamp (soft delete)
  3. Site verdwijnt uit de site switcher en dashboard
  4. Na 14 dagen: hardDeleteExpiredSites() cron verwijdert definitief (R2 bestanden, CF custom domains, DB records)

Settings zijn opgedeeld in 4 subpagina’s, elk met een eigen route en focus:

InstellingBeschrijving
Site naamBewerkbare naam van de site, getoond in site switcher
HomepageWelke pagina wordt getoond op het root-domein
FaviconBrowser tab icoon (upload via media library)
Apple Touch IconiOS home screen icoon
OG ImageDefault social media preview voor alle pagina’s
Site taalContent taal via <html lang=""> (default: nl)
Zoekengine blokkeringZet noindex op alle pagina’s
GevarenzoneSite verwijderen (soft delete met 14 dagen hersteltijd)
InstellingBeschrijving
Site naamWeergavenaam in meta tags
Site titelTitel in browser tab
Site beschrijvingMeta description (site-level fallback)
Social previewOG image en social media instellingen (read-only, instelbaar bij Mijn site)
InstellingBeschrijving
Subdomein{slug}.builtwithbeam.com — automatisch beschikbaar
Custom domainsEigen domein koppelen via CF Pages Custom Domains API
InstellingBeschrijving
TeamledenOverzicht met naam, email, rol, datum
UitnodigingenNieuwe leden uitnodigen per email
RollenbeheerOwner/admin kan rollen wijzigen

Astro 5.8 SSR op Cloudflare Pages. Elke pagina wordt server-side gerenderd bij elk bezoek (met edge caching).

Request flow:

Bezoeker → Cloudflare CDN (edge cache check)
→ Cache hit? → Serveer direct (< 50ms)
→ Cache miss? → CF Pages Worker
→ Host-based routing (custom domain → site lookup via LRU cache)
→ [...slug].astro (catch-all route)
→ Promise.all([getSiteData(), getPageBySlug()])
→ Astro SSR → HTML response
→ Cache: s-maxage=300, stale-while-revalidate=600
FeatureImplementatie
~4 KB JavaScriptAstro islands — geen client-side framework
Inline CSS~18 KB (5.5 KB gzip) — geen render-blocking stylesheet
103 Early HintsFont + LCP hero image preloading
Speculation RulesPrefetch on hover (eagerness: 'moderate')
View TransitionsSmooth navigatie via CSS API
LQIP blur-upBase64 blur → real image cross-fade
Above-fold detectiefetchpriority="high", eager loading, sync decoding
Responsive imagessrcSet met 8 breedtes via CF Image Resizing
  • Meta tags (title, description, canonical URL)
  • Open Graph tags (og:title, og:description, og:image)
  • JSON-LD structured data
  • Robots meta tag (per pagina of site-level)
  • Sitemap (automatisch gegenereerd)

Alle user-generated HTML content wordt gesanitized via isomorphic-dompurify (@beam/shared/sanitize). Zie Technische Stack — Security voor CSP headers en verdere hardening.