@seifer-webapp-factory/contact
v0.1.0
Published
contact — achtste capability-module: een publiek contactkanaal dat een bericht valideert, misbruik afvangt (honeypot + minimale invultijd + throttle), header-injectie onmogelijk maakt en het resultaat als mail aflevert via een geïnjecteerde Mailer. Bezit
Readme
@seifer-webapp-factory/contact
Een publiek contactkanaal als capability-module: valideer een bericht, vang misbruik af, maak header-injectie onmogelijk en lever het af als mail. Bewaart niets — de mailbox van de ontvanger is het systeem van registratie.
Build plan + locked decisions: ../contact.md.
Wat je krijgt
| Deel | Waar | Eigendom |
|---|---|---|
| Contract (DTO's, taxonomie, events, config) | contract/ | dependency (semver) |
| Flow-service, sanitisatie, compositie | backend/src/ | dependency |
| useContactForm() + transport-adapters | frontend/src/ | dependency |
| Neutrale default-stylesheet | frontend/src/contact.css | dependency |
| Host-adapter NestJS · Nitro | backend/templates/{nestjs,nitro}/ | projecteigendom na materialisatie |
| ContactForm.vue, referentiepagina, i18n, tokenbrug | frontend/templates/ | projecteigendom |
Installeren
npm install @seifer-webapp-factory/contactMaterialiseer daarna de surface met de scaffolder (materializeContactModule) en houd alleen de
host-adapter die je nodig hebt.
Bij een gelinkte module: dedupe de kits
Werk je met een file:-link naar deze module (lokale ontwikkeling, vóór publicatie), zet dan in je
Nuxt-config:
build: { transpile: ['@seifer-webapp-factory/kits', '@seifer-webapp-factory/contact'] },
vite: { resolve: { dedupe: ['vue', '@seifer-webapp-factory/kits'] } },Zonder dit resolvet de gelinkte module zijn kit-import naar de node_modules van de monorepo waar hij
vandaan komt, terwijl je eigen componenten die van je project gebruiken. Twee kopieën van de forms-kit
betekent twee verschillende injectie-symbolen, en dan faalt useField met "geen form geprovide" — alleen
tijdens SSR, want client-side rendert het formulier in één keer. Gebruik dedupe en géén alias op de
kale pakketnaam: die omzeilt de exports-map en breekt de subpath-imports van de serverroute.
Na publicatie van de module is dit niet meer nodig.
Twee host-adapters
Deze module is de eerste met een keuze, omdat een contactformulier net zo goed op een brochuresite hoort als in een webapplicatie:
backend/templates/nitro/— een Nuxt 3/4-project zonder NestJS. Zetcontact.post.tsopserver/api/contact/messages.post.tsencontact-deps.tsernaast; daar bind je je mailtransport.backend/templates/nestjs/— een webapp-factory-project.ContactModule.forRoot({ mailer, limiter, config }).
De flow-service eronder is framework-vrij en identiek; de adapters zijn dun. De adapter-e2e draait beide adapters langs dezelfde testgevallen, juist om ze niet uit elkaar te laten lopen.
Importconventie. Anders dan de zeven oudere modules importeren deze templates hun mechanisme via de pakketnaam (
@seifer-webapp-factory/contact/backend) en niet via een repo-relatief pad. Dat is wat ze ook ná het kopiëren naar een project laat kloppen. Imports binnen de surface blijven relatief.
Requires-ports
| Poort | Default | Verplicht |
|---|---|---|
| Mailer | geen — bewust afwezig | ja |
| RateLimiter | in-memory sliding window (single-instance) | nee |
| Clock | systeemklok | nee |
| DesignSystem | neutrale contact.css op het --contact-*-contract | nee |
Er is met opzet geen default-Mailer. Een module die zonder host-configuratie kan mailen is per constructie een open relay.
Config
recipient en sender zijn verplicht; de rest heeft veilige defaults. Zie
contract/config.ts voor alle knoppen: subjectPrefix, limits (per veld plus
een byteplafond), throttle (venster, maximum, sleutelbron), honeypot (veldnaam, minimale invultijd) en
locale. Limieten mogen alleen versmallen: het contract is de bovengrens.
Styling
Drie delen, zoals DESIGN-SYSTEM-PORT.md voorschrijft — en dit is de eerste module die ze alle drie
levert:
- Class hooks —
frontend/CLASS-HOOKS.md - Tokencontract —
frontend/TOKENS.md - Neutrale default-CSS —
import '@seifer-webapp-factory/contact/frontend/contact.css'
Een integratie schrijft alleen de brug: tien tokens mappen, klaar. Zonder brug rendert het formulier neutraal maar correct.
Toegankelijkheid
Elk veld heeft een gekoppeld <label for>, fouten worden aangekondigd (role="alert",
aria-live="polite"), de statusmelding leeft in een role="status"-regio, de verzendknop draagt
aria-busy, en het honeypot-veld staat buiten de tabvolgorde en buiten de toegankelijkheidsboom. De
component-suite draait axe over het formulier, ongestyled. Contrast, focuszichtbaarheid en reduced motion
liggen bij de host — zie ACCESSIBILITY.md en de contrast-checklist in TOKENS.md.
Ejecten
Het formulier anders indelen of een veld toevoegen? Materialiseer ContactForm.vue en bewerk het
(niveau 3 van de divergentieladder). Het mechanisme upgradet daarna gewoon door achter het contract.
Testen
npm test # contract, mechanisme, component + axe, manifest
npm run test:e2e # beide host-adapters over echte HTTPGeen testcontainer, geen Docker: er is geen database.
