@col/email-templates
Versie: 1.0.0 Toegevoegd: 2026-08-06
Herkomst
De blokvolgorde en de zinnen komen uit de drie bestaande activatiemails:
growerportal2/src/lib/email-templates.ts (activationEmailHtml,
resetPasswordEmailHtml), floriday api/src/features/auth/emails/invitation.ts,
en supplier-onboarding-vercel/src/lib/email.ts (sendActivationEmail,
sendPasswordResetEmail). De opbouw bleek in alle drie identiek; alleen de
zinnen verschilden — zie de vergelijkingstabel in
docs/specs/2026-08-06-v8-email-templates-ontwerp.md. Dat is precies de
scheiding waar EmailBlock[] (uit @col/email-shell) voor bestaat: dit item
levert de vorm plus overschrijfbare standaardzinnen, niet vaste tekst.
Waarvoor
De twee mails die elke app met accounts nodig heeft: wachtwoord instellen (na
het aanmaken van een account) en wachtwoord vergeten. Beide functies geven
{ subject, blocks: EmailBlock[] } terug, die je zelf aan buildEmail uit
@col/email-shell doorgeeft.
Niet voor workflow-mails — orderbevestigingen, leveringsmeldingen, statusnotificaties. Die horen in de app zelf.
Let op de naamsverwarring. Supplier-onboarding-vercel's "uitnodiging" vraagt
een externe leverancier een formulier in te vullen — dat is iets anders dan de
activatiemail hier, die hoort bij het aanmaken van een account met wachtwoord.
Alleen Floriday's "uitnodiging" (stel je wachtwoord in) valt onder
activationEmail.
Wat je app moet leveren
EMAIL_TEMPLATES_VERSION
Uit email-fill.ts: EMAIL_TEMPLATES_VERSION: string. Welke versie in een app
draait, achterhaal je met grep -rn EMAIL_TEMPLATES_VERSION src/; bijwerken gaat
met npx shadcn add @col/email-templates --overwrite.
ActivationStrings
| Veld | Type | Betekenis |
|---|---|---|
subject |
string |
Onderwerpregel. Draagt geen merknaam — zie Bekende beperkingen |
heading |
string |
Kop, blok 1 |
greeting |
string |
Aanhef, blok 2. Mag {name} bevatten |
body |
string |
Uitleg, blok 3 |
button |
string |
Knoplabel, blok 4 |
linkValid |
string? |
Geldigheidszin, blok 5. Mag {date} bevatten. Weglaten betekent: geen blok |
ignoreNotice |
string? |
Negeer-zin, blok 6. Weglaten betekent: geen blok |
Meegeleverd: activationStringsNl en activationStringsEn, beide volledig
ingevuld met de standaardzinnen (aanspreekvorm je, zie Bekende beperkingen).
activationEmail
function activationEmail(input: {
name: string
url: string
expires?: string // al geformatteerd; alleen nodig als strings.linkValid om {date} vraagt
strings: ActivationStrings
}): { subject: string; blocks: EmailBlock[] }
De blokkenreeks die eruit komt:
| Volgorde | Blok | Bron |
|---|---|---|
| 1 | heading |
strings.heading |
| 2 | paragraph |
strings.greeting, met {name} ingevuld |
| 3 | paragraph |
strings.body |
| 4 | button |
strings.button + input.url |
| 5 | note |
strings.linkValid met {date} ingevuld — alleen als meegegeven |
| 6 | paragraph |
strings.ignoreNotice — alleen als meegegeven |
ResetStrings / resetEmail
Identiek van vorm aan ActivationStrings / activationEmail, met dezelfde
velden en dezelfde blokkenreeks. Meegeleverd: resetStringsNl en
resetStringsEn. Zie Bewuste keuzes voor waarom dit een apart bestand is in
plaats van een gedeelde implementatie.
Voorbeeld
import { activationEmail, activationStringsNl } from '@/components/email/email-activation'
import { buildEmail } from '@/components/email/email-shell'
const { blocks } = activationEmail({
name: 'Jan',
url: 'https://app.example.com/activate/abc123',
expires: '9 augustus 2026',
strings: activationStringsNl,
})
// De standaard subject ('Stel je wachtwoord in') draagt geen merknaam — zie
// Bekende beperkingen. Overschrijf hem in een gedeelde postbus.
const subject = 'Coloríginz — stel je wachtwoord in'
// EmailBrand komt uit @col/email-shell, niet uit dit item — zie
// docs/items/email-shell.md voor het volledige veldoverzicht.
const brand = {
name: 'Coloríginz',
logo: { filename: 'logo.png', content: logoBuffer, cid: 'brand-logo', width: 140, height: 40 },
accent: '#006799',
accentContrast: '#ffffff',
rule: '#0098da',
footerText: 'Coloríginz — OZ Import BV, Aalsmeer',
}
const mail = buildEmail({
to: 'jan@example.com',
subject,
brand,
// een eigen blok toevoegen kost niets aan de kant van het item —
// dat is precies de reden voor { subject, blocks } in plaats van een compleet Mail
blocks: entraEnabled
? [...blocks, { kind: 'note', text: 'Je kunt ook inloggen met je Microsoft-account.' }]
: blocks,
})
// mail.html, mail.text, mail.attachments -> door naar je eigen transport
Bestanden
| Bestand | Landt in | Soort |
|---|---|---|
email-fill.ts |
components/email/ |
beheerd |
email-activation.ts |
components/email/ |
beheerd |
email-reset.ts |
components/email/ |
beheerd |
Registry-afhankelijkheid: @col/email-shell. Eén npx shadcn add @col/email-templates
installeert dus acht bestanden — deze drie plus de vijf van de romp — allemaal in
components/email/. Dat is precies waarom de relatieve imports kloppen.
Gebruikt door
| App | Sinds versie | Opmerkingen |
|---|---|---|
| (nog geen) | Eerste consument wordt floriday-middleware |
Vul dit bij, ook vanuit een andere repo. Dit is de enige plek waar staat wie geraakt wordt door een wijziging. Installeer je het item in een nieuwe app, voeg dan een regel toe en push naar deze repo — dat hoort bij de installatie.
Bewuste keuzes
{ subject, blocks }en geen compleetMail. Een template die een compleetMailteruggeeft moet eenEmailBranden een ontvanger kennen, en zodra één app iets extra's wil — Floriday toont een SSO-regel, maar alleen als Entra aanstaat — komt er eenextraBlocks?-prop bij, en later eenposition. Met{ subject, blocks }is dat arraywerk in de app ([...blocks, { kind: 'note', text: ... }]), en blijft de template testbaar zonder eenEmailBrandte verzinnen.- Een ontbrekende optionele zin betekent geen blok, niet een leeg blok.
Geen
linkValidmeegeven levert vier blokken op, niet zes met een lege regel. Zo toont Floriday wel een vervaldatum en supplier-onboarding niet, zonder dat daar een boolean-prop voor nodig is. - Het item rekent niet met datums.
expireskomt binnen als kant-en-klare string. Datumopmaak is landinstelling en tijdzone, en dus het werk van de app — net zoalsbuildEmailzelf ook puur blijft. email-activation.tsenemail-reset.tslijken sterk op elkaar en dat blijft zo — ze zijn bewust niet samengevoegd. Beide bestanden worden naar consumerende apps gekopieerd waar ze los van elkaar mogen evolueren; een gedeelde bouwfunctie zou een derde begrip introduceren om een stuk of vijftien regels rechttoe-rechtaan code te besparen.ignoreNoticeis eenparagraph,linkValideennote— geen spiegelbeeldig ontwerp, ondanks dat beide om een "extra zin, alleen als meegegeven" gaan. De shell rendert elkenotein 12px gedempt grijs. ZetignoreNoticeook alsnote, dan staat de geruststelling "je wachtwoord blijft ongewijzigd" in dezelfde voetnootstijl als de vervaldatum van de link — terwijl die zin bedoeld is voor de ontvanger die van de mail schrikt. Eennoteis metadata over de mail, eenparagraphis een zin aan de lezer, enignoreNoticeis het tweede.fillwordt op elke string toegepast, ook opsubjectenbutton, met dezelfde waardenverzameling ({ name, date }), ook al bevatten de meegeleverde teksten daar geen plaatshouder. Dat scheelt een lijstje van welke velden wel en niet ingevuld worden — een lijstje dat bij de eerste extra string alweer niet klopt. Gevolg: een{date}inlinkValidzonder datexpiresis meegegeven laatfilleenErrorgooien in plaats van de plaatshouder letterlijk te laten staan of er een lege string van te maken.
Bekende beperkingen
- Deze twee mails zijn niet apart in een e-mailclient gecontroleerd. De
clientronde die in
docs/items/email-shell.mdstaat vastgelegd — Outlook desktop, 6 aug 2026 — gold de romp: de VML-knop, het CID-logo, de tabelopbouw. Dit item voegt daar geen HTML aan toe; het zet alleen bestaande bloktypen in een andere volgorde. Wil je het toch met eigen ogen zien, dan schrijftnode scripts/build-test-emails.mjsde activatiemail per merk als.emlintmp/, ennode scripts/build-test-emails.mjs resetde reset-mail. Die bestanden komen sinds v8 uit de echte templates en niet meer uit een nagebouwde blokkenreeks. - Alleen Nederlands en Engels. De meegeleverde zinsets zijn
activationStringsNl/EnenresetStringsNl/En. Een derde taal is eigen werk van de consumerende app: een object van dezelfde vorm aanmaken en meegeven aanactivationEmail/resetEmail. - Eén aanspreekvorm:
je. Dat is de toon van Floriday en van growerportal2 in het Engels. Een app dieuwil — zoals supplier-onboarding-vercel — geeft zijn eigenstringsmee; er is geen formele variant meegeleverd. - De bestanden staan in de map van
email-shell(registry/col/email-shell/), niet in een eigenregistry/col/email-templates/. Dat oogt als een opruimpuntje, maar is bewust: een template importeertEmailBlockuitemail-types.ts, en in de consumerende app landen beide items in dezelfde mapcomponents/email/, dus het relatieve pad./email-typesklopt daar. Stond de bron in een eigen map, dan zou datzelfde relatieve pad in déze repo niet kloppen, en zou het item uittsconfig.jsonmoeten — met verlies van typecontrole en tests in de registry zelf. Raak dit niet aan bij een opruimactie zonder eerst de import-structuur te veranderen. - Het onderwerp draagt geen merknaam.
subjectis gelijk aanheading(bv. "Stel je wachtwoord in") enbuildEmailzet er niets voor. In een gedeelde postbus ziet een ontvanger dus een onderwerpregel zonder context over welk portaal het betreft. Een app die dat wil oplossen, geeft zijn eigenstrings.subjectmee — dat is precies waarom de strings een parameter zijn en geen vaste tekst. - De standaardzinnen hebben voorlopig één afnemer. Ze zijn afgeleid uit
drie apps, maar alleen
floriday-middlewareneemt ze naar verwachting ongewijzigd over. Overschrijft die app bij de migratie toch alle zinnen, dan is dat een signaal om te herzien of alleen de vorm gedeeld had moeten worden — kijk daar bij die migratie naar in plaats van het stilzwijgend te laten staan.