feat: polished on-brand HTML confirmation email (task #15)

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
This commit is contained in:
joachimhummel
2026-05-15 16:44:50 +00:00
parent 5fb91deb13
commit 84e0f9ddfd

View File

@@ -75,12 +75,11 @@ router.post("/contact", contactRateLimit, async (req, res) => {
"", "",
"Mit freundlichen Grüßen", "Mit freundlichen Grüßen",
"Joachim Hummel", "Joachim Hummel",
"",
"—",
"jh@unixweb.de",
].join("\n"), ].join("\n"),
htmlContent: ` htmlContent: buildConfirmationEmail(name),
<p>Hallo ${escapeHtml(name)},</p>
<p>vielen Dank für Ihre Nachricht! Ich habe Ihre Anfrage erhalten und melde mich in der Regel innerhalb von <strong>12 Werktagen</strong> bei Ihnen.</p>
<p>Mit freundlichen Grüßen<br />Joachim Hummel</p>
`,
}); });
req.log.info({ to: email }, "Confirmation email sent to sender via Brevo"); req.log.info({ to: email }, "Confirmation email sent to sender via Brevo");
@@ -104,4 +103,115 @@ function escapeHtml(text: string): string {
.replace(/'/g, "&#039;"); .replace(/'/g, "&#039;");
} }
function buildConfirmationEmail(name: string): string {
const safeName = escapeHtml(name);
const brandBlue = "#3f4ff4";
const textDark = "#0f172a";
const textMid = "#334155";
const textMuted = "#64748b";
const textLight = "#94a3b8";
const borderColor = "#e2e8f0";
const bgPage = "#f1f5f9";
const bgCard = "#ffffff";
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>Anfrage erhalten</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;">Vielen Dank, ${safeName}!</p>
<p style="margin:0 0 20px;font-size:13px;font-weight:500;color:${brandBlue};letter-spacing:0.3px;">Ihre Anfrage ist eingegangen.</p>
<p style="margin:0 0 16px;font-size:15px;line-height:1.75;color:${textMid};">Schön, dass Sie sich gemeldet haben. Ich habe Ihre Nachricht erhalten und werde mich in der Regel innerhalb von <strong style="color:${textDark};">12 Werktagen</strong> bei Ihnen melden.</p>
<p style="margin:0 0 32px;font-size:15px;line-height:1.75;color:${textMid};">Bis dahin können Sie gerne mein Portfolio besuchen oder mir direkt eine E-Mail schicken.</p>
<!-- CTA button -->
<table cellpadding="0" cellspacing="0" border="0" role="presentation">
<tr>
<td align="center" style="background-color:${brandBlue};border-radius:8px;">
<a href="https://joachim-hummel.de" target="_blank" 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;">Portfolio ansehen &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>
<!-- Contact links -->
<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>
<!-- Social links -->
<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 versendet bitte antworten Sie nicht direkt darauf.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>`;
}
export default router; export default router;