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:
@@ -53,13 +53,7 @@ router.post("/contact", contactRateLimit, async (req, res) => {
|
|||||||
]
|
]
|
||||||
.filter((l) => l !== undefined)
|
.filter((l) => l !== undefined)
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
htmlContent: `
|
htmlContent: buildNotificationEmail({ name, email, subject, message }),
|
||||||
<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>
|
|
||||||
`,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
req.log.info({ to: "jh@unixweb.de", from: email }, "Contact message sent via Brevo");
|
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, "'");
|
.replace(/'/g, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 →</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};"> </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 & 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};"> • </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};"> • </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 {
|
function buildConfirmationEmail(name: string): string {
|
||||||
const safeName = escapeHtml(name);
|
const safeName = escapeHtml(name);
|
||||||
const brandBlue = "#3f4ff4";
|
const brandBlue = "#3f4ff4";
|
||||||
|
|||||||
Reference in New Issue
Block a user