De klantsite van binnen
apps/site-template is één Astro 7-app in SSR-modus (Node-adapter, poort 4321) met twee gezichten: de publieke site op SITE_DOMAIN en het CMS op admin.<SITE_DOMAIN>. Per klant draait er één container van dezelfde image; alleen de omgevingsvariabelen verschillen. Deze pagina volgt een verzoek door de app en wijst aan waar de code staat.
Configuratie via astro:env
Section titled “Configuratie via astro:env”Alle omgevingsvariabelen staan als schema in astro.config.mjs en worden geïmporteerd uit astro:env/server, nooit uit process.env. Twee soorten:
| Soort | Voorbeelden | Wanneer gelezen |
|---|---|---|
access: "secret" |
TENANT_ID, DATABASE_URL, SITE_DOMAIN, BETTER_AUTH_SECRET, PLATFORM_API_KEY, PLATFORM_WEBHOOK_SECRET, S3-sleutels, RESEND_API_KEY |
Runtime, bij de start van de container |
access: "public" |
EMAIL_FROM, S3_BUCKET, S3_REGION, S3_PUBLIC_URL, S3_FORCE_PATH_STYLE |
Build, ingebakken in de image |
Daarom kan één image alle klanten dienen, en daarom kost het wijzigen van een public variabele een rebuild van iedereen. Zie Deploy.
Twee hosts, één middleware
Section titled “Twee hosts, één middleware”src/middleware.ts beslist per verzoek, in deze volgorde:
- Cross-origin form-POST? Weigeren met 403. Dit vervangt Astro’s eigen
checkOrigin, die achter de proxy het protocol verkeerd ziet (src/lib/csrf.ts). - Statisch bestand of
/api/platform/*? Doorlaten zonder sessie-lookup; de platform-API controleert zelf deX-Platform-Key. - Sessie ophalen via Better Auth en op
context.localszetten. - Acties (
/_actions) lopen op beide hosts en autoriseren zichzelf. - Publieke host:
/admin/...en de auth-pagina’s worden doorgestuurd naar de CMS-host; alle andere paden gaan door naar de publieke pagina’s. - CMS-host: auth-pagina’s zijn zonder sessie bereikbaar; alles anders vereist een gebruiker met CMS-toegang, anders volgt een redirect naar
/login. Schone URL’s zoals/dienstenworden intern herschreven naar/admin/diensten; onbekende paden gaan naar de CMS-home. - Security-headers op elke response (
src/lib/security-headers.ts).
De hostnames komen uit src/lib/hosts.ts: publicHost() haalt protocol en www. weg, adminHost() zet er admin. voor. De regel daarvoor (adminHostnameFor) staat in het contract, zodat het dashboard dezelfde CMS-host afleidt. Lokaal wordt dat admin.localhost:4321.
Routes
Section titled “Routes”Publiek (src/pages/): / (home), /[slug] (pagina’s zoals over-ons), /diensten en /diensten/[slug], /nieuws en /nieuws/[slug], /referenties, /contact, plus /robots.txt en /sitemap.xml. Elke publieke pagina roept na de 404-checks cachePublicRoute(Astro.cache, [tags]) aan; dat is de opt-in voor de route-cache.
CMS (src/pages/admin/), bereikbaar via de secties uit src/lib/cms-nav.ts: Dashboard, Pagina’s, Diensten, Nieuws, Referenties, Media, Navigatie en Instellingen (alleen beheerder). Elke sectie heeft een overzicht, een new-pagina, een detail [id] en voor content met blokken een blok-editor [id]/blocks/[blockId]. Een sectie toevoegen aan cmsNavItems maakt hem meteen routeerbaar; de middleware leidt zijn prefixes daaruit af.
Auth-pagina’s: /login, /logout, /forgot-password, /reset-password. API: /api/auth/[...all] (Better Auth) en /api/platform/health, /api/platform/stats en /api/platform/revalidate voor het dashboard.
Hoe een publieke pagina rendert
Section titled “Hoe een publieke pagina rendert”loadPublicSite()(src/lib/site.ts) haalt desite-singleton (naam, theme, SEO-defaults) en de navigatie op metgetDb(DATABASE_URL).- De pagina haalt zijn eigen content op (bijvoorbeeld de
pages-rij op slug) en zet de media-id’s in de blokken om naar afbeeldingen (src/lib/media.ts). Layout.astrozetthemeToCssVarsString(theme)alsstyleop<html>. Vanaf dat moment bestaan--color-primary,--font-heading,--radius-mden de andere tokens;global.csskoppelt ze via@theme inlineaan Tailwind-utilities.BlockRenderer.astrouit@platform/blocksvalideert de blokken één voor één (parseBlocksForRender) en rendert per blok de bijbehorende component. Ongeldige blokken worden overgeslagen en gelogd; in dev-modus tonen ze eenUnknownBlock.- SEO (
src/lib/seo.ts) combineertseo_defaultsvan de site metseovan de pagina tot titel, beschrijving, canonieke URL en Open Graph.
Zie Blokken en huisstijl voor stap 3 en 4 in detail, en Architectuur, Renderstrategie voor de cache.
CMS-acties
Section titled “CMS-acties”Elke mutatie in het CMS is een Astro Action in src/actions/cms-*.ts (pagina’s, diensten, nieuws, referenties, navigatie, media, instellingen, blokken). Een actie doet altijd hetzelfde rijtje:
- Autoriseren met
requireCmsUser(ofrequireCmsAdminvoor instellingen) uitsrc/lib/cms/. - Valideren met het write-schema uit
@platform/contract(pageWriteSchema,blocksSchema,themeSchemaen zo verder). Formulierdata wordt eerst omgezet insrc/lib/cms/form.tsenblock-form.ts. - Schrijven met Drizzle; constraint-fouten worden nette
CONFLICT-meldingen (src/lib/cms/errors.ts). - Auditlog schrijven (
src/lib/cms/audit.ts). - Cache purgen voor precies wat er is veranderd (
src/lib/cache.ts): per tag of pad metpurgePublicCache; wijzigingen die op elke pagina zichtbaar zijn (navigatie, huisstijl, alt-teksten) purgen de tagpublic. - Gebeurtenis melden aan het dashboard (
content.published,content.deleted) viasrc/lib/platform/send-event.ts; bij een mislukking gaat de envelope inwebhook_outbox. De contactactie meldt op dezelfde manierform.submitted.
Media verwijderen is geblokkeerd zolang een blok, logo of OG-afbeelding er nog naar verwijst (src/lib/cms/media-usage.ts).
Authenticatie en rollen
Section titled “Authenticatie en rollen”Better Auth (src/lib/auth.ts) met de Drizzle-adapter op de tenant-database; tabellen users, sessions, accounts en verifications. Twee rollen uit cmsRoleSchema: admin (beheerder, ook instellingen) en editor (redacteur, alleen content). Publieke registratie staat uit; gebruikers komen uit de seed of uit create-user. baseURL en trustedOrigins zijn de admin-origin, dus sessiecookies gelden alleen op de CMS-host. Een geslaagde login stuurt user.login naar het dashboard via een auth-hook. Inlogpogingen zijn begrensd met src/lib/rate-limit.ts.
Uploads gaan naar een S3-compatible bucket (R2 in productie, MinIO lokaal) onder de prefix <TENANT_ID>/ (src/lib/storage.ts, src/lib/media.ts). De media-rij bewaart key, afmetingen, mime-type en de verplichte alt-tekst. De publieke URL is S3_PUBLIC_URL plus de key; Astro’s /_image-endpoint optimaliseert die afbeeldingen, en astro.config.mjs staat dat toe via remotePatterns afgeleid van S3_PUBLIC_URL.
Platform-API en webhooks
Section titled “Platform-API en webhooks”Onder /api/platform/, alleen met header X-Platform-Key (src/lib/platform/require-platform-key.ts):
GET /health: draait de site en is de database bereikbaar. Gebruikt een aparte, snelle probe met een timeout van twee seconden en antwoordt altijd 200, metdatabase: okofunreachable.GET /stats: aantallen pagina’s, diensten en nieuws, en de laatste mutatie.POST /revalidate: hele route-cache leeg.
Uitgaand ondertekent sendPlatformEvent de envelope met HMAC-SHA256 over timestamp + "." + body en POST hem naar PLATFORM_WEBHOOK_URL. Mislukt dat, dan bewaart webhook_outbox de envelope en probeert drainWebhookOutbox het opnieuw met oplopende wachttijd, tot 24 uur. Het volledige protocol staat in Contract.
Beveiliging in de code
Section titled “Beveiliging in de code”csrf.ts: eigen origin-check metx-forwarded-proto, met uitzondering voor/api/platform/*.security-headers.ts: standaardheaders op elke response.rate-limit.ts: begrenzing van inlogpogingen.safe-redirect.ts: alleen relatieve paden als redirect na login.href.tsin het contract: alleen interne paden enhttp(s)-,mailto- entel-links in navigatie en blokken.- De cache-key bevat de origin, zodat CMS-HTML nooit op de publieke host kan verschijnen, en
cachePublicRoutewordt nooit vanuit CMS-pagina’s aangeroepen.
Losse tests staan naast de code: *.test.ts voor pure logica (bijvoorbeeld csrf.test.ts, outbox.test.ts, send-event.test.ts) en *.integration.ts voor tests tegen een echte database. Er is bewust geen testrunner-configuratie toegevoegd; zie Werkwijze en regels.