10 — Events Addon
Status: 🎨 Vision — Nog niet gebouwd. Afhankelijk van het addon-systeem (05). Tier: Game Pack (functional addon, Laag 2). Aanmelding-model: Extern. Beam genereert geen aanmeldingen of tickets; events linken vanaf de single-page door naar een externe registratie-URL. Vertical packs die Events gebruiken: Yoga Studio, Restaurant, Galerie, en de nog te bouwen Community Hub.
De Events addon brengt evenementen-functionaliteit naar Beam: meetups, workshops, festivals, openingen en webinars. Eénmalig, met of zonder aanmelding via een externe dienst (Eventbrite, Google Forms, Mailchimp, eigen ticketing). Centraal doel: een event publiceren op de site én op Google Events binnen 1 minuut — Schema.org JSON-LD en iCal-feeds zijn first-class burgers.
Functionaliteit die buiten deze scope valt (eigen RSVP, capaciteit, ticketing, recurring, categorie/tag-archieven, maand-archief) staat in §14 Backlog.
Inhoudsopgave
Section titled “Inhoudsopgave”- Filosofie & Scope
- Events vs Bookings — De Scheidslijn
- Aanmelding via Externe URL
- Addon Manifest
- Content Schema
- Blocks (Beam Codex)
- Templates & Routes
- Backend — Crons, Hooks
- SEO & Discoverability
- Synergieën met Andere Addons
- Architectuurintegratie met Beam Core
- Vertical Packs die Events Gebruiken
- Database Schema
- Backlog
- MVP & Implementatie Roadmap
- Open Vragen
1. Filosofie & Scope
Section titled “1. Filosofie & Scope”Een event in Beam is een gebeurtenis met een vaste datum/tijd, een locatie (fysiek of online), en optioneel een externe aanmeldingslink. De host bepaalt wanneer en waar — de bezoeker volgt de aanmeldingslink naar de externe dienst (Eventbrite, Google Forms, eigen platform).
Doelstellingen
Section titled “Doelstellingen”| Doel | Hoe |
|---|---|
| Snel publiceren | Minimaal verplicht: titel, start_at, locatie. Aanmeldings-URL is optioneel. |
| Google Events visibility | Schema.org Event JSON-LD volledig conform spec, inclusief eventStatus en eventAttendanceMode. |
| Kalender-integratie | iCal feed per site (RFC 5545). |
| Beamse uitstraling | Events erven brand tokens, geen eigen styling-eilanden. |
| Geen vendor lock-in | Events exporteerbaar als JSON + iCal. |
| Geen attendee-data in Beam | Aanmeldingen leven extern; Beam slaat geen PII van bezoekers op. |
Wat valt onder Events (v1)
Section titled “Wat valt onder Events (v1)”- Een Yoga-workshop op zaterdag 10:00, registratie via Eventbrite
- Een vernissage op vrijdag 19:00, open inloop, geen aanmelding
- Een online webinar met aanmeldingslink naar Zoom Events
- Een meetup met aanmelding via Meetup.com
Wat valt niet onder Events (v1)
Section titled “Wat valt niet onder Events (v1)”- Een klant boekt een tijdslot voor een 1-op-1 sessie → Bookings
- Een blog post over een afgelopen event → Blog (eventueel met
Events × Blogsynergie) - Een terugkerend openingstijden-rooster → site-instelling
- Eigen aanmelding / ticketing / capaciteit / wachtlijst → §14 Backlog
- Recurring events met RRULE → §14 Backlog
- Categorie- en tag-archieven → §14 Backlog
- Maand-archieven → niet gepland
2. Events vs Bookings — De Scheidslijn
Section titled “2. Events vs Bookings — De Scheidslijn”Beide addons werken met tijd, maar de richting van keuze is omgekeerd.
| Aspect | Events | Bookings |
|---|---|---|
| Wie kiest de tijd? | Host bepaalt vooraf | Klant kiest uit beschikbare slots |
| Aantal momenten | 1 per event | Continu rolling beschikbaarheid |
| Aanmelding | Externe URL (out-of-scope voor Beam) | In Beam (klant kiest slot) |
| Output | Schema.org Event | Schema.org Reservation |
| Distributie | iCal feed, Google Events | Beschikbaarheid via API |
| Voorbeeld | ”Yoga workshop zaterdag 10:00" | "Boek een 1-op-1 yoga sessie” |
Overlap: Een betaalde groepsworkshop met inschrijving past in beide, maar Beam-keuze: Events. Reden: de host bepaalt datum/tijd, de bezoeker volgt de externe aanmeldlink — niet kiest uit slots.
Wanneer beide: Een yoga-studio kan Events gebruiken voor groepslessen (“Yin Yoga zaterdag 10:00, aanmelden via Momoyoga”) én Bookings voor 1-op-1 sessies (“Boek een privéles, kies een tijd”). De synergie Events × Bookings (zie §10) maakt cross-promotie mogelijk.
Architectuurbeslissing: Events en Bookings zijn aparte addons. Ze delen geen tabel, geen URL-prefix en geen capabilities. Wel synergieën. Dit voorkomt dat Bookings te breed wordt en houdt Schema.org/iCal-complexiteit bij Events.
3. Aanmelding via Externe URL
Section titled “3. Aanmelding via Externe URL”Elk event heeft een optioneel registration_url veld. Geen wachtlijst, geen capaciteit, geen ticketing in Beam zelf — dat is bewust uit scope gehouden.
Gedrag op de single page
Section titled “Gedrag op de single page”registration_url is gevuld: → Primaire CTA "Aanmelden" verschijnt op single page + event-card → Link opent in nieuw tabblad (target="_blank") → rel="noopener noreferrer nofollow ugc" → Optioneel iconlabel: externe-link pictogram
registration_url is leeg: → Geen CTA-knop → Event toont alleen info (datum, locatie, beschrijving) → Use case: open inloop, vernissage, gratis meetup zonder aanmeldingSecurity
Section titled “Security”registration_urlgevalideerd viasanitizeUrl()bij save (max 2048 chars)isPrivateUrl()check blokkeert 127.x, 10.x, 172.16-31.x, 192.168.x, 169.254.x, localhost- HTTP automatisch geweigerd, alleen HTTPS toegestaan
- Geen pre-fetch of preview (privacy + voorkomt SSRF leak)
Wat de host elders configureert
Section titled “Wat de host elders configureert”| Concept | Waar |
|---|---|
| Capaciteit / wachtlijst | Op de externe registratiesite |
| Ticket-types & prijzen | Op de externe registratiesite |
| Bevestigings- en herinnerings-emails | Op de externe registratiesite |
| Custom velden bij aanmelding | Op de externe registratiesite |
| Attendee-lijst & check-in | Op de externe registratiesite |
Status-overrides door host
Section titled “Status-overrides door host”Status sold_out blijft als handmatige toggle bestaan — als de externe site uitverkocht is, kan de host dat in Beam markeren, waardoor de “Aanmelden” CTA verdwijnt en Schema.org offers.availability: SoldOut wordt gerenderd.
4. Addon Manifest
Section titled “4. Addon Manifest”Conform §4 van het expansion system:
const eventsManifest: AddonManifest = { id: 'events', name: 'Events', description: 'Evenementen op je site — meetups, workshops, festivals. Aanmelding via externe URL.', icon: 'calendar-days', version: '1.0.0', tier: 'game-pack',
// Content & Rendering blocks: [ eventsListBlock, eventCardBlock, eventCalendarBlock, eventCountdownBlock, eventScheduleBlock, ], collections: [eventsCollection], templates: [eventArchiveTemplate, eventSingleTemplate], routes: [ { pattern: '/events', template: 'events:archive', data: 'events' }, { pattern: '/events/:slug', template: 'events:single', data: 'events:single' }, ],
// Dashboard settings: eventsSettingsSchema, dashboardWidgets: [upcomingEventsWidget], menuItemTypes: [ { id: 'event', label: 'Event', icon: 'calendar' }, { id: 'events-archive', label: 'Events archief' }, ],
// Backend crons: [ { id: 'expire-completed', schedule: '0 * * * *', handler: 'expireCompletedEvents' }, { id: 'archive-cleanup', schedule: '0 9 * * *', handler: 'archiveOldEvents' }, ], publicApiRoutes: [ { method: 'GET', pattern: '/api/events/:id/calendar.ics', handler: 'singleEventIcs' }, ], hooks: [ { event: 'event:published', handler: 'broadcastNewEvent' }, { event: 'event:cancelled', handler: 'broadcastCancellation' }, { event: 'event:rescheduled', handler: 'broadcastReschedule' }, { event: 'event:completed', handler: 'triggerReviewBlock' }, ], emailTemplates: [], // Geen attendee-emails in v1
// SEO seoAssets: [ { type: 'sitemap', route: '/sitemap-events.xml', collection: 'events' }, { type: 'rss', route: '/events/feed.xml', collection: 'events' }, { type: 'json-ld', route: ':event-single', collection: 'events' }, // iCal volgt hetzelfde patroon: { type: 'ical' as const, route: '/events/feed.ics', collection: 'events' }, ],
// Permissions permissions: [ { id: 'events:author', label: 'Event auteur', capabilities: ['events:create', 'events:update'] }, ],
// WoW-systemen specializations: [], // v1: geen specialisaties — zie §14 Backlog provides: [ { type: 'time-bound-content', description: 'Content met datum/tijd, voor synergie met Blog/Popups' }, { type: 'calendar-feed', description: 'iCal feeds voor externe kalenders' }, ], enhancedBy: [ { addonId: 'reviews', capability: 'open-review-form', unlocks: 'Open review-block op event page' }, { addonId: 'blog', capability: 'content-feed', unlocks: '"Aankomende events" sidebar block' }, { addonId: 'bookings', capability: 'time-slots', unlocks: '"Boek intake na workshop" CTA' }, { addonId: 'popups', capability: 'scarcity-popup', unlocks: '"Mis dit event niet" exit-intent popup' }, { addonId: 'portfolio', capability: 'showcase', unlocks: 'Event recap als case study' }, ],}5. Content Schema
Section titled “5. Content Schema”interface EventItem { // Beam core (gedeeld met andere addon items) id: string site_id: string title: string slug: string description_html: string // Sanitized via @beam/shared/sanitize featured_image?: MediaRef status: 'draft' | 'published' | 'cancelled' | 'rescheduled' | 'sold_out' | 'completed' blocks: Block[] // Full Beam editor body voor de single page categories: TaxonomyRef[] // Wel opslaan, geen filter-routes in v1 (zie §14) tags: TaxonomyRef[] // idem created_at: string updated_at: string published_at?: string
// Events-specifieke metadata start_at: string // ISO 8601 — VERPLICHT end_at?: string // ISO 8601 all_day: boolean // Toggle: 'hele dag' (verbergt tijd, niet datum) timezone: string // IANA tz, default 'Europe/Amsterdam'
location: { type: 'physical' | 'online' | 'hybrid' venue_name?: string // 'Yoga Studio Zen' address?: string // 'Prinsengracht 123, 1015 ND Amsterdam' coords?: { lat: number, lng: number } online_url?: string // sanitizeUrl() bij save online_instructions?: string // 'Link verschijnt 1 uur voor start' }
// Externe aanmelding registration_url?: string // sanitizeUrl(), HTTPS-only, max 2048 chars registration_label?: string // Default 'Aanmelden', mag bv. 'Tickets', 'Inschrijven'
// Prijs (puur weergave — Beam verwerkt geen betalingen; mig 093) price?: string | null // Canonieke decimale string '1234.56' of null. UI maskeert naar NL '1.234,56' price_donation?: boolean // true ⇒ 'Op donatiebasis' i.p.v. bedrag; price is dan null price_from?: boolean // true ⇒ 'Vanaf €X' tonen (alleen bij numerieke price)
// Organisator (voor Schema.org) organizer?: { name: string url?: string // sanitizeUrl() email?: string // Voor "Vragen?" mailto, niet voor RSVP }
// Speaker / performer (optioneel, voor Schema.org) performer?: { name: string url?: string }
// Cancellation cancellation_reason?: string // Schema.org vereist dit bij status='cancelled' rescheduled_from?: string // Oorspronkelijke start_at, voor Schema.org previousStartDate}Validatie-regels
Section titled “Validatie-regels”| Regel | Reden |
|---|---|
start_at verplicht | Zonder datum geen event |
end_at >= start_at als gegeven | Logica |
location.type === 'physical' ⇒ venue_name OR address verplicht | Schema.org Place vereist een naam of adres |
location.type === 'online' ⇒ online_url verplicht | Bezoeker moet ergens heen kunnen |
location.type === 'hybrid' ⇒ beide velden verplicht | Schema.org MixedEventAttendanceMode |
registration_url HTTPS-only + sanitizeUrl() + isPrivateUrl() blok | Security |
price alleen cijfers, max 2 decimalen (NL-masking via maskPriceInputNL) | Consistente weergave editor === public |
price_donation === true ⇒ price wordt null, price_from wordt false | Donatiebasis vervangt het bedrag |
price_from alleen actief bij een numerieke price | ”Vanaf” zonder bedrag is betekenisloos |
status === 'cancelled' ⇒ cancellation_reason verplicht | UX + Schema.org best practice |
timezone is valide IANA tz | Anders Schema.org accepteert het niet |
6. Blocks (Beam Codex)
Section titled “6. Blocks (Beam Codex)”Vijf blocks bij activatie. Conform §8 Codex:
| Block | Rarity | Type-affiniteit | Functie |
|---|---|---|---|
events-list | ★ Rare | 🌿 Growth / ⚡ Connect | Upcoming events grid/lijst |
event-card | ★ Rare | ⚡ Connect | Single featured event als hero |
event-calendar | ★ Rare | 💧 Flow | Maand/week-view |
event-countdown | ★ Rare | 🔥 Impact | ”Nog 3 dagen, 12 uur” — launches |
event-schedule | ★ Rare | 💧 Flow | Multi-day programma |
Block-detail per type
Section titled “Block-detail per type”events-list
Section titled “events-list”{ source: 'upcoming' | 'past' | 'manual', manualIds?: string[], limit: number, // Default 6 layout: 'grid' | 'list' | 'cards', columns: 2 | 3 | 4, // Bij grid/cards showImage: boolean, showDate: boolean, showLocation: boolean, showRegistrationButton: boolean, // 'Aanmelden' knop op de card dateFormat: 'short' | 'long' | 'relative', // '23 mrt' / '23 maart 2026' / 'over 5 dagen' emptyStateText: string, // 'Geen aankomende events' // Standaard Beam props: spacing, background, corners, textColorMode}Noot: Filteren op categorie/tag uit de block is uitgesteld naar §14 Backlog. Bron blijft
upcoming,past, ofmanual(handmatig gekozen events) in v1.
event-card
Section titled “event-card”Featured single event. Drie layout-modes (hero, side-by-side, compact). Toont start_at, locatie, korte beschrijving en — indien gevuld — een externe “Aanmelden” CTA.
event-calendar
Section titled “event-calendar”{ view: 'month' | 'week' | 'agenda', showWeekends: boolean, firstDayOfWeek: 1 | 7, // Maandag of zondag highlightToday: boolean, clickBehavior: 'tooltip' | 'navigate', // Tooltip met details OF naar single // Standaard Beam props}Implementatienotitie: De calendar block is client-interactief (klik → tooltip/navigate). Voor Astro public site: hydraten als island (
client:visible). Minimal JS, geen frameworks — vanilla Web Components of een lichte React island.
event-countdown
Section titled “event-countdown”{ eventId: string | 'next', // Specifiek event of automatisch volgende units: ('days' | 'hours' | 'minutes' | 'seconds')[], expiredBehavior: 'hide' | 'show-message' | 'show-next', expiredMessage: string, // Standaard Beam props}event-schedule
Section titled “event-schedule”Multi-day programma met dag-tabs of vertical timeline. Voor festivals en open-deur-dagen.
{ eventId: string, // Het multi-day event view: 'tabs' | 'timeline', groupBy: 'day' | 'track', showSpeakers: boolean, // Vereist Portfolio synergy voor profielen}Secret Blocks (Synergie)
Section titled “Secret Blocks (Synergie)”Onontgrendeld tot de juiste combinatie actief is:
| Combinatie | Secret Block (✧) |
|---|---|
| Events × Blog | events-blog-sidebar — “Aankomende events” in blog post sidebar |
| Events × Reviews | event-reviews-wall — Open review-block op event page |
| Events × Bookings | event-intake-cta — “Plan een intake voor deze workshop” |
| Events × Popups | event-scarcity-popup — “Mis dit event niet” exit-intent |
| Events × Portfolio | event-recap-grid — Afgeronde events als case studies |
7. Templates & Routes
Section titled “7. Templates & Routes”/events → Archive: alle upcoming events/events/[slug] → Single event detail page/events/feed.xml → RSS feed/events/feed.ics → iCal feed/sitemap-events.xml → XML sitemap met <lastmod>Niet in v1: maandarchief, categorie- en tag-filters, categorie-specifieke iCal feeds. Zie §14 Backlog.
Route Resolution
Section titled “Route Resolution”Conform §5.3 van het expansion system:
Bezoeker → /events ↓Bestaat er een custom archief page in pages-tabel met slug 'events'? → JA: render custom page (block editor layout), inject events data → NEE: render eventArchiveTemplate.defaultLayout ↓Dynamic blocks in template resolven: {{events-list}} → grid van komende events {{events-pagination}} → paginering {{events-ical-link}} → "Abonneer op kalender" knopDefault Layout — Archive
Section titled “Default Layout — Archive”const eventArchiveTemplate = { id: 'events:archive', type: 'archive', defaultLayout: { blocks: [ heroBlock({ title: 'Events', subtitle: 'Bekijk onze aankomende evenementen' }), eventsListBlock({ source: 'upcoming', layout: 'cards', columns: 3, limit: 12 }), eventsPaginationBlock(), ctaBlock({ title: 'Op de hoogte blijven?', subtitle: 'Abonneer op onze iCal-kalender', button: 'Kalender toevoegen' }), ], },}Default Layout — Single
Section titled “Default Layout — Single”const eventSingleTemplate = { id: 'events:single', type: 'single', defaultLayout: { blocks: [ eventHeroBlock(), // Image + title + datum + locatie + externe 'Aanmelden' CTA contentBlock({ html: '{{event-description}}' }), eventLocationBlock(), // Map + adres + instructies (online_instructions als online) eventDetailsBlock(), // Organisator, performer, links relatedEventsBlock({ limit: 3 }), eventsCalendarFeedBlock(), // "Toevoegen aan agenda" knoppen ], },}8. Backend — Crons, Hooks
Section titled “8. Backend — Crons, Hooks”Minimaal omdat alle aanmelding-flows extern zijn. Geen public API voor RSVPs, attendees of check-in.
Public API Endpoints
Section titled “Public API Endpoints”| Endpoint | Auth | Doel |
|---|---|---|
GET /api/events/:id/calendar.ics | Public | iCal-file voor single event (“Toevoegen aan agenda”) |
GET /events/feed.ics | Public | Site-brede iCal feed |
GET /events/feed.xml | Public | RSS feed |
| Cron | Schedule | Functie |
|---|---|---|
expireCompletedEvents | 0 * * * * (hourly) | Events met end_at < now() → status='completed', trigger hook |
archiveOldEvents | 0 9 * * * (09:00 daily) | Events > 12 maanden oud → notify host (geen auto-delete) |
Hooks (Event Bus)
Section titled “Hooks (Event Bus)”| Event | Trigger | Standaard handler |
|---|---|---|
event:created | Nieuw event opgeslagen (draft) | — |
event:published | Status → published | KV cache invalidate, BroadcastChannel, Realtime update |
event:cancelled | Status → cancelled | KV cache invalidate, Schema.org status update |
event:rescheduled | start_at verandert | KV invalidate, update calendar feeds met SEQUENCE-increment |
event:completed | Cron promotes naar completed | Trigger Reviews synergy block (geen email) |
event:sold_out | Host markeert handmatig | Update status, verberg externe CTA, render availability:SoldOut |
9. SEO & Discoverability
Section titled “9. SEO & Discoverability”Schema.org Event JSON-LD
Section titled “Schema.org Event JSON-LD”Volledig conform spec — dit is de hoofdreden waarom Events een eigen addon is (Bookings genereert Reservation, niet Event).
<script type="application/ld+json">{ "@context": "https://schema.org", "@type": "Event", "name": "Yoga Workshop: Yin & Restorative", "startDate": "2026-03-23T10:00:00+01:00", "endDate": "2026-03-23T12:00:00+01:00", "eventStatus": "https://schema.org/EventScheduled", "eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode", "location": { "@type": "Place", "name": "Yoga Studio Zen", "address": { "@type": "PostalAddress", "streetAddress": "Prinsengracht 123", "postalCode": "1015 ND", "addressLocality": "Amsterdam", "addressCountry": "NL" } }, "image": ["https://media.builtwithbeam.com/cdn-cgi/image/w=1200,q=80,fit=cover/.../hero.jpg"], "description": "Een rustige tweeurige workshop voor diep ontspannen.", "organizer": { "@type": "Organization", "name": "Yoga Studio Zen", "url": "https://yogastudio.nl" }, "offers": { "@type": "Offer", "url": "https://eventbrite.com/e/yoga-workshop-yin-restorative-...", "availability": "https://schema.org/InStock" }, "performer": { "@type": "Person", "name": "Anna Janssen" }}</script>Belangrijk:
offers.urlverwijst naar de externe registratie-URL — niet naar een Beam-route. Google Events accepteert dit als geldige offer-link.
EventStatus Mapping
Section titled “EventStatus Mapping”| Beam status | Schema.org eventStatus |
|---|---|
published | EventScheduled |
rescheduled | EventRescheduled (vereist previousStartDate) |
cancelled | EventCancelled (vereist description met reden) |
sold_out | EventScheduled + offers.availability: SoldOut |
completed | EventScheduled met endDate in verleden |
EventAttendanceMode Mapping
Section titled “EventAttendanceMode Mapping”| Beam location.type | Schema.org eventAttendanceMode |
|---|---|
physical | OfflineEventAttendanceMode |
online | OnlineEventAttendanceMode (+ location: VirtualLocation) |
hybrid | MixedEventAttendanceMode (+ beide locaties) |
iCal Feed (RFC 5545)
Section titled “iCal Feed (RFC 5545)”BEGIN:VCALENDARVERSION:2.0PRODID:-//Beam//Events//NLMETHOD:PUBLISHNAME:Yoga Studio Zen — EventsX-WR-CALNAME:Yoga Studio Zen — EventsX-WR-TIMEZONE:Europe/AmsterdamREFRESH-INTERVAL;VALUE=DURATION:PT1HX-PUBLISHED-TTL:PT1H
BEGIN:VEVENTUID:event-abc123@yogastudio.nlDTSTAMP:20260223T140000ZDTSTART;TZID=Europe/Amsterdam:20260323T100000DTEND;TZID=Europe/Amsterdam:20260323T120000SUMMARY:Yoga Workshop: Yin & RestorativeDESCRIPTION:Een rustige tweeurige workshop... Aanmelden via https://eventbrite.com/...LOCATION:Yoga Studio Zen\, Prinsengracht 123\, AmsterdamURL:https://yogastudio.nl/events/yoga-workshop-23-mrtSTATUS:CONFIRMEDORGANIZER;CN=Yoga Studio Zen:mailto:info@yogastudio.nlEND:VEVENT
END:VCALENDARURLwijst naar de Beam single page (niet de externe registratie), zodat agenda-clients de detail-pagina openenREFRESH-INTERVAL: PT1H— kalender-clients ververen elk uurUIDuniek per event, stabiel over wijzigingenSTATUSmapped:CONFIRMED/CANCELLED/TENTATIVE(rescheduled)- Reschedule →
STATUS:CONFIRMEDmetSEQUENCEincrement, zodat clients de wijziging oppikken
OG/Twitter Cards
Section titled “OG/Twitter Cards”<meta property="og:type" content="event" /><meta property="event:start_time" content="2026-03-23T10:00:00+01:00" /><meta property="event:end_time" content="2026-03-23T12:00:00+01:00" /><meta property="event:location:locality" content="Amsterdam" /><meta name="twitter:card" content="summary_large_image" />Analytics Bus Events
Section titled “Analytics Bus Events”Conform §2 van het expansion system:
| Event | Properties |
|---|---|
event_view | event_id, source (organic, archive, related) |
event_registration_click | event_id, destination_host (eventbrite.com, etc.) |
event_ical_subscribed | scope (site, single) |
event_share | event_id, channel (email, twitter, copy-link) |
10. Synergieën met Andere Addons
Section titled “10. Synergieën met Andere Addons”Conform §7 van het expansion system. Synergie-matrix v1:
| Combinatie | Unlock |
|---|---|
| Events × Blog | • events-blog-sidebar block (aankomende events in blog post) • {{upcoming-events}} inline rich-text element • “Event recap” post-template • Auto-link events naar gerelateerde blog posts |
| Events × Reviews | • event-reviews-wall block — open review-formulier op event detail page • Reviews zichtbaar op volgend event (“Vorig jaar: 4.8/5”) • Geen email-prompt (Beam heeft geen attendee-lijst) |
| Events × Bookings | • “Boek 1-op-1 intake voor deze workshop” CTA op single event • Pre-event consultation slots • Post-event follow-up sessie aanbieden |
| Events × Popups | • Site-level “Mis dit event niet” exit-intent popup • Geo-targeted “Event in jouw stad” |
| Events × Portfolio | • Event als case study na afloop • event-recap-grid block met portfolio-stijl tegels • Auto-import event foto’s → portfolio item |
Niet in v1: Events × Forms (custom RSVP-velden) en Events × Shop (ticketed events) — beide leven extern. Zie §14 Backlog.
Synergie Voorbeeld — Events × Reviews
Section titled “Synergie Voorbeeld — Events × Reviews”Gebruiker activeert Events + Reviews ↓Systeem detecteert: events.enhancedBy bevat { addonId: 'reviews', capability: 'open-review-form' } ↓Synergie ontgrendeld: → Nieuw block: 'event-reviews-wall' beschikbaar in single event template → Open formulier: "Was je hier? Laat een review achter" → Reviews gekoppeld aan event_id, aggregaat ★ gemiddelde ↓Codex melding: "Events + Reviews actief — bezoekers kunnen reviews achterlaten op event-pagina's"11. Architectuurintegratie met Beam Core
Section titled “11. Architectuurintegratie met Beam Core”Brand Tokens
Section titled “Brand Tokens”Events erven brand tokens uit brand_profiles — geen eigen palet. Specifieke token-mappings:
| Element | Token |
|---|---|
| Event card achtergrond | surface |
| Event card border | border |
| Event datum/tijd accent | accent |
| ”Aanmelden” CTA-knop | primary button style |
| Status badge “Sold out” | error.bg + error.text |
| Status badge “Geannuleerd” | warning.bg + warning.text |
| Calendar today highlight | accent met 12% opacity |
KV/ISR Cache
Section titled “KV/ISR Cache”Conform .claude/rules/data-fetching.md en ADR-008:
// API Worker (authoritative writer)const kvKeys = { eventsList: (siteId: string) => `events:list:${siteId}`, event: (siteId: string, slug: string) => `events:single:${siteId}:${slug}`, eventsIcs: (siteId: string) => `events:ics:${siteId}`, eventsRss: (siteId: string) => `events:rss:${siteId}`,}
// Invalidatie bij elke mutatie:async function onEventChanged(siteId: string, event: EventItem) { await Promise.all([ api.cache.delete(kvKeys.eventsList(siteId)), api.cache.delete(kvKeys.event(siteId, event.slug)), api.cache.delete(kvKeys.eventsIcs(siteId)), api.cache.delete(kvKeys.eventsRss(siteId)), ])}Realtime Sync
Section titled “Realtime Sync”Conform .claude/rules/realtime.md:
// Dashboard (editor)const channel = new BroadcastChannel('beam-events-updated')channel.postMessage({ siteId, eventId, action: 'updated', timestamp: Date.now() })
// Cross-device:supabase.channel('events-changes') .on('postgres_changes', { event: '*', schema: 'public', table: 'addon_items', filter: `site_id=eq.${siteId}`, }, handler) .subscribe()RLS Policies
Section titled “RLS Policies”-- Lees: iedereen in het team mag events lezenCREATE POLICY "events_read" ON addon_items FOR SELECT USING ( collection_id IN (SELECT id FROM addon_collections WHERE site_id IN ( SELECT id FROM sites WHERE owner_id = get_team_owner_id((select auth.uid())) )));
-- Schrijf: editors+CREATE POLICY "events_write" ON addon_items FOR ALL USING ( collection_id IN ( SELECT c.id FROM addon_collections c JOIN sites s ON c.site_id = s.id JOIN team_members tm ON tm.owner_id = s.owner_id WHERE tm.user_id = (select auth.uid()) AND tm.role IN ('owner', 'admin', 'editor', 'events:author') ));Geen attendee-data: Omdat aanmeldingen extern leven, slaat Beam geen
event_rsvpsop. Geen public-insert policies, geen PII-exposure, geen Turnstile, geen retentie-beleid voor bezoekers.
Image Optimization
Section titled “Image Optimization”Featured images via shared image-cache.ts (.claude/rules/images.md):
getBackgroundImageUrl()voor event-hero blocksgetImageSrc()+getImageSrcSet()voor event-card blocks- LQIP via
blur_data_urluitmedia.blur_data_url - CF Image Resizing widths: 480/640/768/960/1280/1440
Sanitization
Section titled “Sanitization”| Veld | Methode |
|---|---|
description_html | sanitizeHtml() via DOMPurify (dashboard + Astro) |
online_url, registration_url, organizer.url, performer.url | sanitizeUrl() (max 2048, HTTPS-only) + isPrivateUrl() blok |
online_instructions | sanitizeHtml() met whitelist <p>, <a>, <br> |
cancellation_reason | Plain text only (geen HTML) |
registration_label | Plain text only, max 40 chars |
Multi-language
Section titled “Multi-language”Events vertaalbaar op item-niveau (core taxonomy):
- Eén event per taal of één event met
translationsmap (open vraag, zie §16) - iCal feed per taal:
/events/feed.ics?lang=nl - Schema.org
inLanguageveld populeren - Datum/tijd-formattering per locale (
Intl.DateTimeFormat)
Privacy & GDPR
Section titled “Privacy & GDPR”- Geen attendee-data in Beam — alle aanmelding-PII leeft op de externe registratiesite, wat Beam’s GDPR-oppervlak drastisch verkleint
- Wel data: organisator-email (publiek opgegeven door host) — geen retentie-beleid nodig
event_viewanalytics zijn anoniem (geen IP-opslag)- Externe registration-clicks worden geteld met
event_registration_click(anoniem)
12. Vertical Packs die Events Gebruiken
Section titled “12. Vertical Packs die Events Gebruiken”| Vertical Pack | Archetype | Voorbeeld events | Externe registratie via |
|---|---|---|---|
| Yoga Studio (bestaat) | Healer × Guide | Workshops naast vaste lessen | Momoyoga, Eversports |
| Restaurant (bestaat) | Host × Curator | Chef’s table, themadiners | Formitable, eigen formulier |
| Galerie (uitbreiding Photographer) | Creator × Curator | Vernissages, exposities | Geen registratie (open inloop) |
| Community Hub (nieuw) | Host × Entertainer | Meetups, borrels, netwerk-events | Meetup.com, Eventbrite |
| Business Coach (bestaat) | Builder × Guide | Webinars, masterclasses | Zoom Events, Eventbrite |
| Photographer (bestaat) | Creator × Storyteller | Foto-walks, workshops | Eventbrite |
Conferentie Site (Scholar × Curator) is uit de vertical pack-roadmap verwijderd zolang Ticketed Events niet beschikbaar is. Zie §14 Backlog.
13. Database Schema
Section titled “13. Database Schema”Conform 02-brand-json-spec § Database. Events leeft op het bestaande addon_collections + addon_items schema, met events-specifieke kolommen via metadata JSONB.
Geen nieuwe tabellen in v1
Section titled “Geen nieuwe tabellen in v1”In tegenstelling tot eerdere ontwerpen worden event_rsvps en event_email_log niet geïntroduceerd in v1. Beide horen bij eigen RSVP-functionaliteit en staan in §14 Backlog.
Bestaande Tabellen — Geen Wijzigingen Nodig
Section titled “Bestaande Tabellen — Geen Wijzigingen Nodig”| Tabel | Hoe Events het gebruikt |
|---|---|
addon_collections | Eén row: id=events, slug='events', metadata_schema=eventMetadataSchema |
addon_items | Eén row per event. Events metadata in metadata JSONB |
addon_taxonomies | Categorieën en tags opgeslagen, niet filterbaar via routes in v1 |
addon_archive_pages | Custom /events page override |
content_references | Cross-content: blog post ↔ event, event ↔ portfolio recap |
site_milestones | Vertical pack voortgang (bijv. “Eerste event toegevoegd”) |
Performance Considerations
Section titled “Performance Considerations”addon_items.metadata->>'start_at'queries → expression index:CREATE INDEX idx_events_start_atON addon_items ((metadata->>'start_at'))WHERE collection_id IN (SELECT id FROM addon_collections WHERE slug = 'events');- iCal-feed materialiseert in KV — geen DB hit op cache-hit
- Default
LIMIT 200op alle list-queries (conform.claude/rules/security.md)
14. Backlog
Section titled “14. Backlog”Functionaliteit die bewust uit v1 is gehouden. Volgorde indicatief, geen harde commitment.
B-1 Eigen RSVP (specialisatie “RSVP Events”)
Section titled “B-1 Eigen RSVP (specialisatie “RSVP Events”)”In plaats van uitsluitend extern linken zou Beam zelf aanmeldingen kunnen verwerken.
Vervangt of vult registration_url aan met:
- Capaciteit + wachtlijst
- Aanmeldformulier met basisvelden (naam, email, +1)
- Bevestigings-email (
rsvp-confirmed) - Reminder-email 24u voor start (cron)
- Cancel via tokenized email-link
- Host dashboard: attendees-lijst + export CSV
- Database:
event_rsvpstabel +event_email_log(idempotentie) - Public API:
POST /api/events/:id/rsvpmet Turnstile + rate-limit - Crons:
sendReminders24h,promoteFromWaitlist - Permissions:
events:host - GDPR: retentie-beleid voor attendee-PII (default 12 maanden)
B-2 Ticketed Events (specialisatie “Ticketed Events”)
Section titled “B-2 Ticketed Events (specialisatie “Ticketed Events”)”Bouwt op B-1 en vereist Shop addon:
- Ticket-types met prijzen
- Shop checkout integratie
- QR-tickets als digital product
- Check-in API + scanner-PWA
- Refund flow
- Permissions:
events:checkin - Schema.org
offers.price/priceCurrency
B-3 Recurring Events (RFC 5545 RRULE)
Section titled “B-3 Recurring Events (RFC 5545 RRULE)”- RRULE editor in event-form
- Recurring event expansion in feeds + blocks
- Multi-day schedule met dag-tabs (block bestaat al, RRULE-data ontbreekt)
- Exception dates (EXDATE) management
- Schema.org expansion per instance
B-4 Forms Synergie — Custom RSVP-velden
Section titled “B-4 Forms Synergie — Custom RSVP-velden”Vereist B-1. Custom velden in RSVP-form (dieetwensen, t-shirt maat, +1 details). Form responses gekoppeld aan attendee record.
B-5 Shop Synergie — Ticket-sales
Section titled “B-5 Shop Synergie — Ticket-sales”Dekt B-2.
B-6 Reviews via Email-prompt
Section titled “B-6 Reviews via Email-prompt”Vereist B-1 (Beam heeft dan attendee-lijst). Post-event review-prompt email via cron. Voor v1: alleen open review-block op event page (geen email).
B-7 Categorie- en Tag-filters
Section titled “B-7 Categorie- en Tag-filters”- Routes:
/events/categorie/[slug],/events/tag/[slug] - Filter-bar block op archive-page
events-listblock uitbreiden metsource: 'category' | 'tag'+categoryId/tagIdprops- Categorie-specifieke iCal feeds:
/events/categorie/[slug]/feed.ics
B-8 Maand-archief
Section titled “B-8 Maand-archief”Niet gepland — gebruikers zoeken bij events bijna nooit per maand vooruit; ze willen weten “wat komt er”. Hier expliciet uit scope gehouden.
B-9 Conferentie Site Vertical Pack
Section titled “B-9 Conferentie Site Vertical Pack”Vereist B-2 (Ticketed). Op de plank zetten tot Shop + Ticketed beschikbaar zijn.
"Conferentie Site" Vertical Pack (backlog)│├── Archetype: Scholar × Curator├── Requires: Events (ticketed), Shop, Portfolio, Forms├── Style Pack: "Scholar Authority"├── Niche Blocks: Speaker Grid, Multi-day Schedule, Sponsor Wall, Venue Map└── Milestones: speaker, programma, ticket-types, eerste verkoop, conferentie liveB-10 Webhook-integraties
Section titled “B-10 Webhook-integraties”Push events naar externe Mailchimp/Eventbrite/Google Calendar bij event:published. Out-of-scope tot er vraag is.
B-11 Geavanceerde Codex synergie-blocks
Section titled “B-11 Geavanceerde Codex synergie-blocks”events-blog-sidebar,event-intake-cta,event-scarcity-popup,event-recap-gridzijn gepland in v1 (zie §6)ticket-checkout(vereist B-2)event-custom-rsvp(vereist B-4)
15. MVP & Implementatie Roadmap
Section titled “15. MVP & Implementatie Roadmap”MVP — Sprint 1 (1-2 weken)
Section titled “MVP — Sprint 1 (1-2 weken)”Scope: Events met externe registratie-URL, het noodzakelijke om Schema.org en iCal werkend te krijgen.
| # | Taak | Output |
|---|---|---|
| 1 | Manifest + collection registratie | Addon zichtbaar in dashboard activatie-UI |
| 2 | Event-editor metadata-form (datum, locatie, registration_url, organisator) | Editor kan event aanmaken |
| 3 | Twee blocks: events-list + event-card met externe CTA-knop | Te plaatsen op pages |
| 4 | Archive template /events + single /events/[slug] | Public site routes werken |
| 5 | Schema.org Event JSON-LD op single | Google Events ready |
| 6 | iCal feed /events/feed.ics + /api/events/:id/calendar.ics | Kalender-abonnement werkt |
| 7 | OG/Twitter meta-tags voor events | Social sharing werkt |
| 8 | Sitemap inclusief events | SEO-discovery |
| 9 | KV cache invalidatie hooks | Public site refresh werkt |
| 10 | Crons: expireCompletedEvents, archiveOldEvents | Status-housekeeping |
| 11 | E2E tests: create event → publish → public site → iCal validatie | Quality gate |
Niet in MVP: RSVP, capaciteit, recurring, ticketing, calendar block, countdown, multi-day schedule, categorie/tag filters, attendee-emails. Zie §14 Backlog.
Fase 2 — Block-uitbreiding & RSS (1 week)
Section titled “Fase 2 — Block-uitbreiding & RSS (1 week)”| # | Taak |
|---|---|
| 12 | event-calendar block (maand/week view, hydrated island) |
| 13 | event-countdown block |
| 14 | event-schedule block (multi-day, statisch, geen RRULE) |
| 15 | RSS feed /events/feed.xml |
Fase 3 — Synergie-blocks (1-2 weken)
Section titled “Fase 3 — Synergie-blocks (1-2 weken)”| # | Taak |
|---|---|
| 16 | Blog synergy: events-blog-sidebar + {{upcoming-events}} inline |
| 17 | Reviews synergy: event-reviews-wall open review-form |
| 18 | Bookings synergy: event-intake-cta |
| 19 | Popups synergy: event-scarcity-popup |
| 20 | Portfolio synergy: event-recap-grid |
Fase 4+ — Zie Backlog
Section titled “Fase 4+ — Zie Backlog”Eigen RSVP (B-1), recurring (B-3), categorie-filters (B-7) en verder.
16. Open Vragen
Section titled “16. Open Vragen”| # | Vraag | Beslissing nodig voor |
|---|---|---|
| OQ-E1 | Timezone-handling: user’s browser tz of altijd event.timezone? Default Europe/Amsterdam versus auto-detect. | MVP |
| OQ-E2 | Multi-language events: één item met translations map, of één item per taal met translation_group_id? | Fase 3+ |
| OQ-E3 | Bij verzetten event: stuur SEQUENCE-update naar iCal subscribers of een aparte cancel+create? | MVP |
| OQ-E4 | Event-categorieën opslaan in addon_taxonomies (gedeeld met blog) of events-specifiek? Filter-routes komen pas in B-7. | MVP |
| OQ-E5 | Toont event-card block “Externe link” pictogram naast CTA, of voelt dat te techy? UX-keuze. | MVP |
| OQ-E6 | Past sanitizeUrl() ook een nofollow SEO-attribuut toe op registration_url links, of vertrouwen we de host? | MVP |
| OQ-E7 | Hoe verwerken we event:rescheduled als de cancel-mail in B-1 niet bestaat? Alleen iCal SEQUENCE-update? | MVP |
Open vragen rond eigen RSVP / Ticketed / Recurring zijn verplaatst naar de relevante backlog-items in §14.
Bijlage A — Voorbeeld: Hoe Een Event Wordt Aangemaakt
Section titled “Bijlage A — Voorbeeld: Hoe Een Event Wordt Aangemaakt”User klikt "+ Nieuw event" in dashboard ↓Editor opent met events-specifieke metadata-form: • Titel: "Yoga Workshop: Yin & Restorative" • Slug: "yoga-workshop-23-mrt" (auto-slugify) • Datum: 2026-03-23 • Tijd: 10:00 — 12:00 (timezone: Europe/Amsterdam) • Locatie: Fysiek • Venue: "Yoga Studio Zen" • Adres: "Prinsengracht 123, 1015 ND Amsterdam" • Aanmelding: https://eventbrite.com/e/yin-restorative-... (label: "Aanmelden via Eventbrite") • Featured image: upload of Unsplash • Organisator: Yoga Studio Zen ↓Hoofdblock-editor: voeg blocks toe voor de detail-pagina ↓"Publiceren" klikken ↓Onder water: → INSERT INTO addon_items → KV cache invalidate (eventsList, eventIcs, eventsRss) → BroadcastChannel + Supabase Realtime notify → Hook 'event:published' fires → analytics event ↓Direct daarna: → /events archief toont nieuwe event → /events/yoga-workshop-23-mrt single page live met Schema.org JSON-LD → /events/feed.ics bevat nieuwe VEVENT (offers.url naar Eventbrite) → Google bot kan event indexeren binnen 24uBijlage B — Anti-patronen
Section titled “Bijlage B — Anti-patronen”Specifieke valkuilen voor de Events addon, naast de algemene anti-patronen in .claude/rules/data-fetching.md:
| Anti-patroon | Waarom niet | Correct alternatief |
|---|---|---|
Event als gewone page met Block[] zonder eigen collection | Geen Schema.org, geen iCal, geen filters | Gebruik events collection met metadata schema |
Timezone uit user’s browser opslaan in start_at | Verschillende attendees zien andere tijden | Sla start_at op met expliciete timezone veld, render via Intl.DateTimeFormat |
registration_url zonder sanitizeUrl() + isPrivateUrl() | XSS via javascript: URL, SSRF via private IP | Altijd valideren in API én client |
registration_url link zonder target="_blank" + rel="noopener noreferrer nofollow ugc" | Phishing surface, SEO-vervuiling | Strikt rel-attribuut afdwingen |
Cancellation zonder Schema.org eventStatus update | Google blijft cancelled event als live tonen | Update eventStatus: EventCancelled + cancellation_reason |
iCal feed zonder REFRESH-INTERVAL | Apple/Google Calendar ververst maandelijks i.p.v. uurlijks | Altijd REFRESH-INTERVAL:PT1H of korter |
| Eigen attendee-tabel toevoegen “voor later” | Dode kolommen, RLS-complexiteit, GDPR-oppervlak | Wachten tot B-1 daadwerkelijk gebouwd wordt |
Categorie/tag in events-list block toevoegen “voor later” | Cluttered UI, dode prop in editor | Wachten tot B-7 daadwerkelijk gebouwd wordt |