Skip to content

Changelog

Solo afbeelding-block (image) met:

  • Breedte: 4 presets — small (col-span-6), normal (col-span-8, default), large (col-span-12 / 960px), fullscreen (viewport-bleed via 100vw + negatieve margin trick)
  • Hoogte: 4 presets — landscape (16:9), portrait (3:4), square (1:1), original (vrije hoogte met object-fit: contain, default) → matcht het ColumnImageHeight enum uit styles.ts
  • Caption + alt zitten op het media-object zelf (props.image), niet als aparte sidebar-velden. MediaObjectField heeft nu imagePickerShowAlt + imagePickerShowCaption opt-in props die inline inputs onder de preview rendert.
  • Optionele link wrap rondom de figure
  • Focal point wordt overgenomen via image picker (zelfde object-shape als background images)
  • Astro renderer: 0 client-side JS, BlurImage met LQIP, <figcaption> voor a11y, aboveFoldpriority eager-loading
  • preloadBlockImages extractor uitgebreid voor image-blocks (kind: 'inline')

Week 4 (21 apr) — Testimonials block-package

Section titled “Week 4 (21 apr) — Testimonials block-package”

Drie nieuwe block types voor klantbeoordelingen, alle drie via één gedeelde packages/shared/src/testimonials/ module zodat card/quote/rating-styling exact identiek is tussen variants:

  • testimonials-grid — 2 of 3 kolommen, optionele mobile-carousel via Embla
  • featured-testimonial — enkele gecentreerde quote (0 JS op publieke site)
  • testimonials-carousel — Embla-gedreven slideshow met arrows + dots

Architectuur en infrastructuur:

  • Embla Carousel als gedeelde carousel-engine (ADR-009). Code-split via React.lazy() + dedicated vendor-embla chunk; pagina’s zonder carousel betalen geen ~7 KB gzip kosten.
  • Shared rendering module met types.ts, classes.ts (class-builders) en review-card.css. React (editor + public) en Astro renderen via dezelfde classnames; geen drift tussen frameworks.
  • Pre-work — repeater compact mode in content-sections.tsx: useEffect- synced refs + memo() op SortableRepeaterItem voorkomen dat 11 sibling- items re-renderen bij elke keystroke. Compact-state per field-instance in localStorage. Layout-shift bij toggle = 0px (header op vaste Y-positie).
  • Pre-work — preloadBlockImages refactor naar per-block-type extractor (columns + 3 testimonial blocks + group/columns recursie). Inline images preloaden single src tot 200px (geen volle 1920px srcSet voor 40px avatars).

Editor-UX-verbeteringen:

  • EditableText.keepEmpty prop — element blijft contentEditable bij lege value zodat user kan blijven typen na alles te wissen.
  • FieldDefinition.visibleWhen accepteert tweede blockProps argument → per-item velden kunnen gaten op block-level toggles (bv. Rating-veld alleen zichtbaar als showRating aan).
  • NumberStepper (alleen /+-knoppen, step 0.5) — gebruikt voor rating.
  • MediaObjectField (type image-picker) — slaat volledig media-object op { url, media_id, blur_data_url, alt } i.p.v. enkel een URL-string.

Visuele consistentie:

  • Card-stijl card altijd witte surface + shadow + donkere tekst, ongeacht block-tekstmodus. Specificity (0,2,0) overschrijft Tailwind text-white op light-blocks zodat de kaart-tekst leesbaar blijft.
  • Card-stijl bordered border-color = currentColor (volgt block-tekstkleur).
  • .review-featured overschrijft padding niet; surface-eigenschappen komen van --card/--plain/--bordered. Author-rij is verticale stack: avatar/ logo, naam, rol/bedrijf gecentreerd.
  • Avatar + logo hebben dezelfde 40×40 footprint zodat meta-text op gelijke x-positie begint. Half-ster ratings (4.5) via twee-laags CSS overlay.
  • Carousel pijltjes: position absolute buiten viewport (3.5rem in section- padding), z-index 10, witte solid surface met shadow.
  • Grid + carousel + grid-overlay debug allemaal 1rem kolom-gap.
  • slidesToScroll: 1 + containScroll: 'trimSnaps' → per slide scrollen, dots = unieke views (geen dode dots).
  • Quote-mark none rendert echt niets (geen fallback decoratief teken).
  • 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 op carousel-viewport.

Quality gates:

  • 4 kritieke + 11 belangrijke audit-bevindingen (van parallelle beam-reviewer, Performance Benchmarker en Senior Developer agents) opgelost vóór merge.
  • 10 minor findings + UX-verfijningen verwerkt naar P3 backlog of doorgevoerd.
  • docs/block-types.md, docs/functionaliteit.md, docs/glossarium.md, docs/backlog.md bijgewerkt. ADR-009 toegevoegd.

Editor Code Quality & Accessibility Audit:

  • Dead code verwijderdeditor.tsx (ongebruikt, vervangen door page-editor.tsx)
  • History memory cappushHistory() helper beperkt undo-stack tot 50 entries, voorkomt onbeperkt geheugengebruik
  • History dedupJSON.stringify vergelijking vervangen door updatedAt check (O(1) ipv O(n))
  • Store selectorsToolbar en Canvas gebruiken nu granulaire selectors (usePageMeta, useHistoryState, usePageBlocks) ipv volledige store destructuring
  • Code duplicatieuseInlineEditor() hook (hover-to-edit), useEditableButtons() hook (button modal), resolvePatternBlocks() helper (canvas drop) geëxtraheerd
  • API validatieblocks veld in pages API: z.unknown() vervangen door typed schema met max 200 blocks; cascade-hrefs endpoint: Zod validatie toegevoegd
  • DOMPurify hardeningurl() waarden in style attributes worden gestript (tracking pixel preventie)
  • Accessibility — WAI-ARIA tablist op sidebar tabs, aria-label op toolbar knoppen, tabIndex + keyboard handlers op blocks, <main> + role="application" landmarks, block picker <div><button>, toolbar zichtbaar bij keyboard focus

Bunny Stream Video Upload (TUS Protocol):

  • TUS resumable upload — Video’s worden direct naar Bunny CDN geüpload via TUS protocol. Beam API genereert SHA256 presigned headers via crypto.subtle (Web Crypto API)
  • Client-side direct upload — Videobestand gaat niet via de API Worker, voorkomt payload-limieten en CPU-tijd
  • useVideoEncodingPoll() hook — Batch status polling met exponential backoff (2s → 30s), ref pattern voor stabiele callbacks, optimistic SWR cache update bij encoding completion
  • Encoding status — Polling via batch API endpoint (PATCH /media/video/:id/status)

Button Kleur Systeem:

  • colorToken vervangt hardcoded kleuren — Buttons gebruiken nu light, dark, auto (past aan op achtergrond), of brand tokens (primary, secondary, accent + shades) in plaats van hex waarden
  • CSS injection preventiesanitizeColorToken() valideert tokens tegen een whitelist
  • color veld deprecated — Bewaard als fallback voor backwards compatibiliteit

Database migraties: 058 (backfill colorToken: 'primary' op buttons met oude hardcoded paarse kleur), 059 (idem, resterende gevallen)

Rich Text Editor — Expand Modal & Performance:

  • Single editor instance — Eén Tiptap editor die verplaatst wordt tussen canvas en modal (geen sync nodig)
  • Expand modal — Sidebar-knop opent tekst in uitklapbare modal met FixedToolbar + EditorContent
  • Static HTML tijdens animaties — Canvas en modal tonen statische HTML snapshot tijdens open/close animaties; EditorContent mount/unmount gebeurt buiten animatieframes (~50ms ProseMirror overhead ontkoppeld van 60fps)
  • Deferred store writes — Bewerkingen in modal schrijven niet continu naar store; flush gebeurt pas bij sluiten
  • Frozen height — Canvas behoudt hoogte wanneer editor naar modal verplaatst (synchrone DOM meting tijdens render)

Modal Component Verbeteringen:

  • Focus trap (WCAG 2.1 §2.4.3) — Tab/Shift+Tab cyclet binnen modal, inclusief [contenteditable] elementen
  • aria-labelledby via contextModal genereert automatisch een titleId en deelt deze via React context; ModalHeader leest en koppelt het ID automatisch (werkt voor alle 17 modal consumers)
  • Escape stack — Module-level stack zodat alleen de bovenste modal reageert op Escape (gestapelde modals)
  • Scroll lock counter — Reference counter voorkomt race conditions bij gestapelde modals
  • Exit animatieonExitComplete callback op AnimatePresence voor deferred cleanup na exit transition
  • Event isolatiestopPropagation() op backdrop mousedown/click voorkomt sidebar clearing

Performance Optimalisaties (RichText):

  • Zustand selectors — Individuele selectors i.p.v. useEditorStore() destructuring; voorkomt re-renders bij ongerelateerde store updates
  • state.page bail-out — Zustand subscription skipt getNestedValue traversal als state.page referentie ongewijzigd (~80% reductie)
  • Echo prevention Map — Per-editor write timestamps i.p.v. single globals; veilig bij concurrent editors
  • Cleanup op unmount — rAF cancellation, _richTextActiveCount leak fix, orphaned expandEditorPath opruiming

Performance Optimalisaties (Toolbar):

  • shouldShow module-level — Stabiele functiereferentie voorkomt onnodige BubbleMenu recalculaties
  • DOM leak fixuseBubbleAnimation cleanup roept previousRemove() aan bij unmount tijdens exit-animatie
  • Dead codehandleLinkClick vereenvoudigd

Tekst Kleur Auto-detectie:

  • Image/video achtergrondengetTextColorClasses() default nu naar lichte tekst bij afbeelding/video achtergronden; alleen donkere tekst bij lichte overlay met >30% opacity

Overig:

  • useMemo in useTextColorClasses — Voorkomt nieuwe object allocatie bij elke render
  • Lege gradient stops guard — Expliciete check voor lege stops array in gradient detectie

Rich Text Editor (Hero Block):

  • Tiptap WYSIWYG editor — Hero block titel en subtitel gebruiken nu een Tiptap rich text editor met bubble menu in plaats van plain text EditableText
  • Bubble menu — floating toolbar bij tekst selectie: vet, cursief, onderstrepen, doorhalen, link, heading levels (H1-H4), paragraaf, text style presets (Lead, Accent, Klein)
  • Link bewerkenLinkEditModal met twee tabs: pagina selectie (PagePicker) of vrije URL invoer
  • Lazy loading — Tiptap dependencies in apart vendor-editor chunk (~135KB gzipped), geladen via React.lazy()
  • Zustand history integratie — Tiptap’s built-in undo/redo uitgeschakeld, store history is single source of truth
  • Backwards compatibel — bestaande plain text content wordt automatisch gewrapped in <p> tags via normalizeToHtml()
  • Sidebar vereenvoudigd — tag en size dropdowns verwijderd uit hero sidebar, heading keuze zit nu in bubble menu
  • Vite chunk splittingmanualChunks omgeschreven naar functie-form (fix voor pnpm hoisting)

Fix: Nested <a> tags in preview cards:

  • usePreviewMode() hook — Public block renderers (hero, buttons, cta, columns) detecteren of ze binnen een PatternPreview renderen en tonen buttons als <span> in plaats van <a>, waardoor ongeldige geneste anchor tags voorkomen worden die React rendering konden crashen

Header Navigatie Configuratie:

  • Twee navigatie layoutslogo-menu-button (Logo—Menu—CTA) en menu-logo-menu (Menu—Logo—Menu)
  • CTA Button — configureerbaar via ButtonEditModal uit de editor, met brand kleuren en stijlen (solid/outline/transparent)
  • Header Design Configurator — achtergrond (brand kleuren + transparantie), border & schaduw, positie (sticky/static/fixed), hoogte (compact/default/tall), typografie per element
  • Hide on scroll — header verdwijnt bij scrollen, verschijnt bij omhoog scrollen. CSS transition: transform 240ms ease-out
  • Transparant bij hero — header wordt transparant als eerste block een achtergrondafbeelding/video heeft. Server-side gerenderd (geen JS flash). Tekstkleur past automatisch aan (licht/donker detectie)
  • Live preview — sticky browser mockup met BrowserMockup component (CSS zoom voor sticky header support). Morph animatie naar full-width via Motion layoutId. Skeleton → content fade-in
  • Multi-logo upload — Brand → Elementen: meerdere logo varianten (Primary, Wit, Donker, Icoon). In header config selecteerbaar via selectedLogoId
  • Brand kleuren in headerColorPresetRow met shade picker (long-press/hover). Wit + transparant als extra opties
  • Sites query optimalisatiesite_domains join op de sites query: primary domain beschikbaar vanaf frame 1 (geen aparte fetch)

Database migraties: 049 (multi-logo), 050 (brand public read RLS), 055 (header nav type + design), 056 (brand logo), 057 (RLS header_menu_left_id)

AI Content Generatie:

  • Per-block AI generatie — ✨ sparkle knop per block in hover-toolbar, popover met intent input, smart merge (content wijzigen, styling behouden)
  • Smart merge strategie — AI wijzigt alleen content (tekst, labels, items). Buttons behouden style/color/colorToken. Afbeeldingen en achtergronden onaangeroerd
  • Verfijnen vs vervangen — AI bepaalt automatisch of content aangepast of volledig vervangen wordt op basis van de prompt
  • Per-block style context — AI kent layout-instellingen (kolombreedte, achtergrond type, aantal items) per block type en past tekst lengte aan
  • Bestaande content als context — Huidige teksten meegestuurd zodat AI kan verfijnen
  • Brand Story tab — 5 card-secties op de Merk pagina: bedrijfsinfo, doelgroep, missie/waarden, tone of voice (3 sliders + aanspreekvorm), contactgegevens
  • AI Block Schema — Machine-readable beschrijving van alle 7 block types met metadata, richtlijnen en voorbeelden (packages/shared/src/ai/block-schema.ts)
  • AI Hero Test — 3 varianten van hero titel + subtitel via POST /ai/generate-hero
  • Security hardening — Unicode newline sanitize, Anthropic client key rotation, tool-name check, brand context max 3000 chars, Zod output validatie, IDOR check, 25s timeout
  • AI rate limit — 10 req/min per user
  • Documentatiedocs/ai-architectuur.md (dataflow, merge strategie, security), docs/ai-block-schema.md (per-block velden en verbeterpunten)

Settings Herstructurering:

  • 4 subpagina’s — Settings opgesplitst in Mijn site (/settings), SEO (/settings/seo), Domeinnamen (/settings/domains) en Gebruikers (/settings/users)
  • Mijn site — Site naam, homepage, site taal, afbeeldingen (favicon, touch icon, OG image), zoekmachines, gevarenzone
  • SEO — Metadata, titel, social preview (read-only) — nieuw: SettingsSeoPage.tsx
  • Domeinnamen — Subdomein + custom domains — nieuw: SettingsDomainsPage.tsx
  • Gebruikers — Team management verplaatst van /users naar /settings/users, site-scoped

Site-scoped Team Management (migraties 051-053):

  • owner_id verwijderd uit team_members en team_invitationssite_id is de primaire scope
  • get_user_site_ids() vervangt get_team_owner_id() voor site-scoped queries
  • Bootstrap trigger add_owner_to_site_team voegt owner automatisch toe als team member bij site creatie
  • Backfill migratie (053) — Alle bestaande sites krijgen owner in team_members als dat nog ontbrak

KV + ISR Cache Layer:

  • KV namespace BEAM_CACHE gebonden aan zowel API als public-site
  • Cache keys: host:{hostname}, site:{siteId}, page:{siteId}:{slug}, brand-css:{siteId}
  • Write-through: API schrijft naar KV bij publicatie, public site leest eerst uit KV
  • Instant invalidatie bij publicatie (geen timer-based cache)
  • ~1-5ms reads vs ~80-150ms DB reads

P0 Security Fixes:

  • Unbounded queries.limit() toegevoegd aan 8 dashboard queries (domains, patterns, pages, menus, media, media_tags)
  • Pages PUT mass assignment — Zod validatie schema blokkeert user_id, site_id, block_count, created_at
  • site_settings UNIQUE constraint — Voorkomt duplicate rijen per site (was oorzaak favicon bug)

Database fixes:

  • UNIQUE constraint op site_settings.site_id — voorkomt duplicaat rijen bij concurrent inserts

Database migraties: 047 (header nav type), 049-053 (RLS, site-scoped teams, backfill, site_settings UNIQUE)

Multi-site Support:

  • Meerdere sites per owner — Max 10 actieve sites per owner, enforced via check_site_quota() DB trigger
  • Site switcher — In user dropdown met favicon en sitenaam. Wisselen herlaadt alle site-scoped data
  • Site aanmaken — Modal met naam invoer en auto-slug generatie. Bootstrap maakt automatisch settings en brand profile aan
  • Soft deleteDELETE /sites/:id zet deleted_at timestamp. 14 dagen grace period. hardDeleteExpiredSites() cron ruimt R2, CF custom domains en DB op
  • Site naam bewerkbaar — In settings pagina, direct opslaan
  • Site-scoped data — Alle SWR hooks geparametriseerd op siteId. Menus en site_settings hebben site_id kolom. Pages slug UNIQUE per site
  • Gecentraliseerde authgetSessionUserId() in lib/auth.ts vervangt 3 duplicate getUserId helpers
  • Service layer refactorsiteId parameter verplicht op settings, menus, pages service functies. Geen localStorage fallbacks meer

Header Navigation Types + Brand Logo:

  • Header nav typesHeaderNavType en HeaderDesign interfaces in shared types. site_settings uitgebreid met header_nav_type, header_menu_left_id, header_button_text/url, header_design kolommen
  • Brand logologo_url kolom op brand_profiles tabel

Database migraties: 046 (multi-site), 047 (header nav type + menus/settings site scoping), 048 (brand logo), 049-050 (RLS policies)

Design Token Systeem Optimalisatie:

Design Token Systeem Optimalisatie:

  • Variable fonts — Self-hosted statische fonts (62 bestanden, 3 weights per font) vervangen door 19 variable fonts + 3 statische fallbacks (Poppins, Lato, Proza Libre). Alle weights 100-900 correct gerenderd. <link rel="preload"> elimineert FOUT. CSP blijft font-src 'self'
  • Shade preview — Kleurvarianten sectie in Colors tab met OKLCH shade scale (50-900) per brand kleur, click-to-copy
  • BrandColorToken shades — Type uitgebreid met shade varianten (primary-50 t/m accent-900). Contrast vars voor alle 30 shades. Blocks kunnen shade varianten gebruiken als achtergrondkleur
  • Shade picker in editor — Expandable shade strip in block color settings na klik op brand kleur
  • Performance fixesgenerateShadeScale() memoization met input validation, SSR brand CSS fetch timeout (3s), shade CSS vars in realtime sync

Brand Identity UI Uitbreiding:

  • 7 brand kleuren — BrandColors uitgebreid met neutral en surface naast primary, secondary, accent, textDark, textLight. Alle 6 mood presets bijgewerkt met unieke neutral/surface waarden
  • Vrije kleurkiezer — ColorInput component met native color picker + hex text input per kleur. Vervangt de oude preset-only selectie
  • WCAG contrast badges — Client-side checkWcagContrast() per kleurveld met inline pass/fail badge (ratio + level). Suggestie voor accessible alternatieven bij te laag contrast
  • Typography fijnafstelling — Heading weight/letter-spacing/line-height sliders, body weight/line-height sliders, type scale selector (compact/standaard/dramatisch)
  • Export tab — 3 export kaarten (CSS Custom Properties, W3C Design Tokens JSON, Tailwind v4 @theme) met kopieer-naar-klembord en open-in-browser
  • Versiegeschiedenis modal — Snapshot opslaan (label input), versie-lijst met relatieve timestamps, restore met audit logging
  • Accessibility hardening — WAI-ARIA tabs pattern (keyboard nav, aria-selected, tabpanel), alle form controls met labels, aria-live regions voor toasts en status updates, aria-hidden op decoratieve iconen, ContrastBadge als role=“status”
  • Security audit fixessafeFontName regex uitgebreid met (, dubbele --brand-surface CSS var fix, generateDarkModeTokens deduplicatie, version restore audit log

Eerdere features deze week:

  • Semantic token laag — Automatisch afgeleide tokens: --brand-surface, --brand-border, --brand-text-muted, --brand-on-primary, --brand-danger/success/warning/info. Semantic tokens in CSS + W3C JSON + dark mode overrides.
  • OKLCH shade generationgenerateShadeScale() genereert 50-900 kleurschaal per brand kleur via OKLCH kleurruimte.
  • Dark mode auto-generatie@media (prefers-color-scheme: dark) block in brand CSS met automatisch berekende dark mode tokens via OKLCH.
  • WCAG AA contrast checkingcheckWcagContrast(), getContrastRatio(), suggestAccessibleColor() in shared. API endpoint GET /brand/:siteId/contrast.
  • W3C Design Token JSON exportGET /brand/:siteId/tokens in W3C Design Tokens draft spec format.
  • Tailwind v4 @theme exportGET /brand/:siteId/tokens/tailwind.
  • Token versioningtoken_versions tabel met auto-snapshot trigger. SECURITY DEFINER met SET search_path hardening.
  • Brand color tokensBrandColorToken type voor achtergronden en buttons met var(--brand-{token}) CSS custom properties.
  • Self-hosted Google Fonts — 21 brand font families als self-hosted woff2 (19 variable + 3 static fallbacks), CSP font-src 'self' only.
  • Brand realtime syncuseBrandRealtimeSync() hook: BroadcastChannel + Supabase Realtime.
  • CF Image Resizing fix — Non-R2 URLs routeren door media.builtwithbeam.com.
  • Font FOUT fix — Lazy font loading via IntersectionObserver, canvas opacity transitie.
  • Vision docs alignment — 03-design-tokens.md consistent gemaakt met 01/02
  • Documentatie consolidatie — Alle docs samengevoegd in Starlight docs site met 7 secties + vision
  • Router workerdocs.builtwithbeam.com routering naar docs Pages project
  • Tech debt opgelost — Alle openstaande tech debt items afgehandeld
  • E2E test documentatie — CLAUDE.md en TECHNICAL_OVERVIEW.md bijgewerkt
  • Layered Context Architecture — 4-lagen systeem: hooks, rules, agents, context files
  • Project agents — beam-reviewer, beam-block-builder, beam-migration, beam-api-builder, beam-preflight
  • Scaffold scriptinit-claude alias voor herbruikbare project setup
  • Fase 3 voltooid — Vite SPA dashboard op CF Pages
  • ESLint cleanup — 172 warnings naar 0
  • Sentry integratie — Error monitoring over alle 3 apps
  • E2E tests — 21 Playwright tests (auth, pages, editor, media, smoke)
  • Rate limiting — Sliding window per user (100/min general, 20/min uploads)
  • Structured logging — JSON logs met request IDs
  • Blur placeholders — LQIP via CF Image Resizing
  • Fase 2 voltooid — Hono API op CF Workers
    • 8 route modules (pages, patterns, media, team, domains, stock-photos, cache, health)
    • Bearer token auth + rate limiting
    • R2 media pipeline met deduplicatie
    • Team management met rollen en uitnodigingen
    • Custom domains via CF Pages Custom Domains API
  • Fase 1 voltooid — Astro publieke site op CF Pages

    • 8 server-rendered block components
    • Host-based routing voor custom domains
    • CF Image Resizing voor responsive afbeeldingen
    • CSP headers en XSS sanitisatie
    • Performance: ~4KB JS, inline CSS, 103 Early Hints
  • Fase 0 voltooid — Security hardening

    • RLS op alle tabellen
    • SSRF preventie op externe media URLs
    • XSS preventie via DOMPurify
    • CSP headers op publieke site
    • Bootstrap trigger voor nieuwe gebruikers