Task #17: Send a branded notification email to Joachim on contact form submission

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
This commit is contained in:
joachimhummel
2026-05-15 17:11:43 +00:00
parent a834aaed55
commit 3ef76a4433

View File

@@ -53,13 +53,7 @@ router.post("/contact", contactRateLimit, async (req, res) => {
]
.filter((l) => l !== undefined)
.join("\n"),
htmlContent: `
<p><strong>Name:</strong> ${escapeHtml(name)}</p>
<p><strong>E-Mail:</strong> ${escapeHtml(email)}</p>
${subject ? `<p><strong>Betreff:</strong> ${escapeHtml(subject)}</p>` : ""}
<hr />
<p style="white-space:pre-wrap">${escapeHtml(message)}</p>
`,
htmlContent: buildNotificationEmail({ name, email, subject, message }),
});
req.log.info({ to: "jh@unixweb.de", from: email }, "Contact message sent via Brevo");
@@ -103,6 +97,164 @@ function escapeHtml(text: string): string {
.replace(/'/g, "&#039;");
}
function buildNotificationEmail({
name,
email,
subject,
message,
}: {
name: string;
email: string;
subject?: string;
message: string;
}): string {
const safeName = escapeHtml(name);
const safeEmail = escapeHtml(email);
const safeSubject = subject ? escapeHtml(subject) : null;
const safeMessage = escapeHtml(message);
const brandBlue = "#3f4ff4";
const textDark = "#0f172a";
const textMid = "#334155";
const textMuted = "#64748b";
const textLight = "#94a3b8";
const borderColor = "#e2e8f0";
const bgPage = "#f1f5f9";
const bgCard = "#ffffff";
const bgField = "#f8fafc";
return `<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Neue Kontaktanfrage</title>
</head>
<body style="margin:0;padding:0;background-color:${bgPage};font-family:'Inter','Helvetica Neue',Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" style="background-color:${bgPage};">
<tr>
<td align="center" style="padding:48px 16px 40px;">
<table width="560" cellpadding="0" cellspacing="0" border="0" role="presentation" style="max-width:560px;width:100%;">
<!-- ── Header ── -->
<tr>
<td align="center" style="background-color:${brandBlue};border-radius:12px 12px 0 0;padding:32px 40px 28px;">
<table cellpadding="0" cellspacing="0" border="0" role="presentation">
<tr>
<td align="center" style="width:44px;height:44px;background-color:rgba(255,255,255,0.18);border-radius:10px;font-size:18px;font-weight:700;color:#ffffff;letter-spacing:-0.5px;line-height:44px;">
JH
</td>
</tr>
</table>
<p style="margin:10px 0 0;font-size:13px;font-weight:600;color:rgba(255,255,255,0.80);letter-spacing:1.5px;text-transform:uppercase;">Joachim Hummel</p>
</td>
</tr>
<!-- ── Body ── -->
<tr>
<td style="background-color:${bgCard};padding:40px 40px 32px;">
<p style="margin:0 0 6px;font-size:22px;font-weight:700;color:${textDark};line-height:1.3;">Neue Kontaktanfrage</p>
<p style="margin:0 0 28px;font-size:13px;font-weight:500;color:${brandBlue};letter-spacing:0.3px;">Jemand hat das Kontaktformular ausgefüllt.</p>
<!-- Sender details -->
<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" style="border:1px solid ${borderColor};border-radius:8px;overflow:hidden;margin-bottom:20px;">
<!-- Name -->
<tr>
<td style="padding:12px 16px;background-color:${bgField};border-bottom:1px solid ${borderColor};">
<p style="margin:0 0 2px;font-size:11px;font-weight:600;color:${textLight};letter-spacing:1px;text-transform:uppercase;">Name</p>
<p style="margin:0;font-size:15px;font-weight:600;color:${textDark};">${safeName}</p>
</td>
</tr>
<!-- Email -->
<tr>
<td style="padding:12px 16px;background-color:${bgField};border-bottom:1px solid ${borderColor};">
<p style="margin:0 0 2px;font-size:11px;font-weight:600;color:${textLight};letter-spacing:1px;text-transform:uppercase;">E-Mail</p>
<p style="margin:0;font-size:15px;color:${textMid};"><a href="mailto:${safeEmail}" style="color:${brandBlue};text-decoration:none;">${safeEmail}</a></p>
</td>
</tr>
${
safeSubject
? `<!-- Subject -->
<tr>
<td style="padding:12px 16px;background-color:${bgField};border-bottom:1px solid ${borderColor};">
<p style="margin:0 0 2px;font-size:11px;font-weight:600;color:${textLight};letter-spacing:1px;text-transform:uppercase;">Betreff</p>
<p style="margin:0;font-size:15px;color:${textDark};">${safeSubject}</p>
</td>
</tr>`
: ""
}
<!-- Message -->
<tr>
<td style="padding:12px 16px;background-color:${bgCard};">
<p style="margin:0 0 6px;font-size:11px;font-weight:600;color:${textLight};letter-spacing:1px;text-transform:uppercase;">Nachricht</p>
<p style="margin:0;font-size:15px;line-height:1.75;color:${textMid};white-space:pre-wrap;">${safeMessage}</p>
</td>
</tr>
</table>
<!-- CTA button -->
<table cellpadding="0" cellspacing="0" border="0" role="presentation">
<tr>
<td align="center" style="background-color:${brandBlue};border-radius:8px;">
<a href="mailto:${safeEmail}?subject=Re%3A%20${safeSubject ? encodeURIComponent(subject ?? "") : encodeURIComponent(`Ihre Anfrage`)}" style="display:inline-block;padding:13px 28px;font-size:14px;font-weight:600;color:#ffffff;text-decoration:none;letter-spacing:0.2px;border-radius:8px;">Absender antworten &rarr;</a>
</td>
</tr>
</table>
</td>
</tr>
<!-- ── Divider ── -->
<tr>
<td style="background-color:${bgCard};padding:0 40px;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation">
<tr>
<td style="font-size:0;line-height:0;border-bottom:1px solid ${borderColor};">&nbsp;</td>
</tr>
</table>
</td>
</tr>
<!-- ── Footer ── -->
<tr>
<td style="background-color:${bgCard};border-radius:0 0 12px 12px;padding:24px 40px 32px;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation">
<tr>
<td>
<p style="margin:0 0 2px;font-size:14px;font-weight:700;color:${textDark};">Joachim Hummel</p>
<p style="margin:0 0 14px;font-size:13px;color:${textMuted};">Webentwicklung &amp; Softwarelösungen</p>
<p style="margin:0 0 10px;font-size:13px;color:${textLight};">
<a href="mailto:jh@unixweb.de" style="color:${brandBlue};text-decoration:none;">jh@unixweb.de</a>
<span style="color:${borderColor};">&nbsp;&bull;&nbsp;</span>
<a href="https://joachim-hummel.de" target="_blank" style="color:${brandBlue};text-decoration:none;">joachim-hummel.de</a>
</p>
<p style="margin:0;font-size:13px;color:${textLight};">
<a href="https://blog.unixweb.de" target="_blank" style="color:${brandBlue};text-decoration:none;">Blog</a>
<span style="color:${borderColor};">&nbsp;&bull;&nbsp;</span>
<a href="https://n8n.io/creators/jhummel/" target="_blank" style="color:${brandBlue};text-decoration:none;">n8n Creators</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- ── Legal note ── -->
<tr>
<td align="center" style="padding:20px 0 0;">
<p style="margin:0;font-size:12px;color:${textLight};line-height:1.6;">Diese E-Mail wurde automatisch durch das Kontaktformular generiert.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>`;
}
function buildConfirmationEmail(name: string): string {
const safeName = escapeHtml(name);
const brandBlue = "#3f4ff4";