Replaced the minimal plain-HTML notification email (sent to jh@unixweb.de) with a
fully branded HTML email that matches the existing confirmation email layout.
Changes:
- Added `buildNotificationEmail()` function in `artifacts/api-server/src/routes/contact.ts`
- Same visual structure as `buildConfirmationEmail()`: blue header with JH monogram,
white card body, divider, branded footer with links
- Sender details (name, e-mail, subject, message) are displayed in a structured field
table inside the card body, with subtle background shading and label rows
- "Absender antworten →" CTA button links to `mailto:{email}?subject=Re: {subject}`
so Joachim can reply to the sender in one click
- Subject field row is conditionally rendered (omitted when no subject was provided)
- All user-supplied values are escaped through the existing `escapeHtml()` helper
- The first `sendTransacEmail` call now passes `buildNotificationEmail(...)` instead
of the inline minimal HTML string
No schema, API contract, or dependency changes. Typecheck passes cleanly after
running codegen to resolve a pre-existing import gap in @workspace/api-zod.
Replit-Task-Id: 601aa064-22da-4553-9ad0-a15f82563eb6
Replaced the plain 4-line HTML confirmation email with a fully styled,
table-based HTML email that matches Joachim Hummel's portfolio branding.
Changes:
- artifacts/api-server/src/routes/contact.ts
- Added buildConfirmationEmail(name: string): string helper function
- Replaced inline htmlContent template literal with a call to the new helper
- Extended textContent plain-text fallback with email/separator footer line
Email design details:
- Full DOCTYPE + <html lang="de"> structure for broad client compatibility
- All styles are inline (no <style> blocks) — compatible with Gmail, Outlook, Apple Mail
- Table-based layout throughout (no CSS grid/flexbox, no <div> dividers) for Outlook
- Divider uses a zero-height table cell with border-bottom instead of a <div>
- Brand blue header (#3f4ff4 ≈ hsl(234 89% 60%)) with frosted "JH" monogram badge
- White card body on slate-100 page background matching portfolio palette
- Personalised greeting using the sender's name (XSS-safe via escapeHtml)
- Blue accent sub-heading "Ihre Anfrage ist eingegangen."
- 1–2 Werktage response-time promise in bold
- CTA button "Portfolio ansehen →" linking to joachim-hummel.de
- Footer with name, role tagline, email + website links, and social links
(Blog at blog.unixweb.de and n8n Creators profile — both present in portfolio)
- Legal disclaimer note at the bottom
No new dependencies introduced. Pre-existing typecheck errors in the file
(missing express-rate-limit types, missing api-zod export) are unrelated to
this change and pre-date this task.
Replit-Task-Id: fd961a0e-5bc4-4d29-a6c3-e90227307fe0
Task #11: Formular gegen Spam schützen
- Installed `express-rate-limit` (^8.5.2) as a runtime dependency in
`@workspace/api-server`
- Created a rate limiter (5 requests per IP per hour, 1-hour sliding window)
using `rateLimit()` from express-rate-limit
- Applied the limiter as inline middleware on POST /contact so it runs before
the handler
- On limit exceeded the API returns HTTP 429 with a German-language JSON error:
{ success: false, message: "Zu viele Anfragen. Bitte versuchen Sie es in
einer Stunde erneut." }
- Uses `standardHeaders: "draft-8"` (RateLimit header group) and disables
legacy X-RateLimit-* headers
- Added `app.set("trust proxy", 1)` in app.ts so that Express reads the real
client IP from X-Forwarded-For (set by Replit's reverse proxy), ensuring
the rate limit is applied per actual client rather than per proxy IP
- No other changes to the contact handler flow
No deviations from the task description.
Replit-Task-Id: de2cecbd-511f-4046-8e87-567ec96e19fb
After a visitor submits the contact form, Brevo now sends two emails:
1. The existing notification to jh@unixweb.de with the message details
2. A new confirmation email to the sender's address
The confirmation email:
- Is sent from "Joachim Hummel <jh@unixweb.de>"
- Addresses the sender by name
- Thanks them and sets response-time expectations (1-2 Werktage)
- Includes both plain-text and HTML versions
- Is logged separately via req.log.info for observability
If either email fails, the entire request returns a 500 error (atomic
behavior — both succeed or neither does from the user's perspective).
File changed:
- artifacts/api-server/src/routes/contact.ts
Also ran `pnpm --filter @workspace/api-spec run codegen` to fix a
pre-existing typecheck failure caused by stale generated lib output.
Typecheck passes cleanly after codegen.
Replit-Task-Id: a5d51157-6bd1-48c7-ba04-68e7d951eeab