fix(mail): Willkommensmail-Kopf ohne Bildzwang – Logo als HTML, Welle als Streifen

Rueckmeldung des Nutzers: in Outlook nur ein grosser schwarzer Kasten, kein
Logo (das eingebettete Kopfbild wurde nicht angezeigt; Logo und Schriftzug
steckten nur darin). Jetzt Bildmarke aus Tabellenzellen und Schriftzug als
Text in einer niedrigen dunklen Leiste, darunter die Duenen-Welle als
600x56-Streifen, der ins Weiss auslaeuft; fehlt das Bild, bleibt nur Abstand.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 16:29:16 +02:00
parent 32441d77c7
commit 52f538c432
4 changed files with 77 additions and 36 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 10 KiB

+20 -24
View File
@@ -1,28 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="300" viewBox="0 0 1200 300">
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="112" viewBox="0 0 1200 112">
<!--
Kopfbild der Tessera-Systemmails (Willkommensmail). Quelle des PNG
daneben (welcome-header.png, 1200x300, angezeigt 600x150); erzeugt mit
`node apps/api/scripts/render-mail-header.mjs`.
Wellenstreifen unter dem Kopf der Tessera-Systemmails (Willkommensmail).
Quelle des PNG daneben (welcome-header.png, 1200x112, angezeigt 600x56);
erzeugt mit `node apps/api/scripts/render-mail-header.mjs`.
Motiv: der Dashboard-Hintergrund "Duenen" in seiner dunklen Fassung
(apps/web/src/lib/dashboard-background.ts, dunes('#1a1c20', '#23262b',
'#2c3036', '#363a41')), auf das flache Mail-Format 4:1 umgelegt, mit
einer feinen gelben Wellenlinie als Akzent; links die Bildmarke
(apps/web/src/app/icon.svg, Grundplatte mit heller Kontur wie auf der
dunklen App-Leiste) und der Schriftzug.
Seit quick-260930 (Rueckmeldung des Nutzers: in Outlook "ein riesiger
schwarzer Fleck, kein Logo") steckt KEIN Logo und KEIN Text mehr im Bild:
Bildmarke und Schriftzug stehen als HTML im Mailkopf und erscheinen immer.
Dieses Bild ist nur noch Schmuck: oben die Kopffarbe, darunter die
Duenen-Wellen des Dashboard-Hintergrunds (dunkle Fassung,
apps/web/src/lib/dashboard-background.ts) mit der feinen gelben Linie,
unten laeuft es ins Weiss der Karte aus. Zeigt ein Mailprogramm das Bild
nicht, bleibt dort nur weisser Abstand.
-->
<rect width="1200" height="300" fill="#1a1c20"/>
<path d="M0 176 C230 136 470 222 700 190 S1060 118 1200 160 L1200 300 L0 300Z" fill="#23262b"/>
<path d="M0 176 C230 136 470 222 700 190 S1060 118 1200 160" fill="none" stroke="#ffed00" stroke-opacity="0.55" stroke-width="3"/>
<path d="M0 230 C270 196 500 266 780 232 S1090 190 1200 214 L1200 300 L0 300Z" fill="#2c3036"/>
<path d="M0 272 C300 250 600 296 900 268 S1130 252 1200 260 L1200 300 L0 300Z" fill="#363a41"/>
<g transform="translate(60 40) scale(1.5)">
<rect x="4" y="4" width="64" height="64" rx="14" fill="#1a1a1a" stroke="#ffffff" stroke-opacity="0.22" stroke-width="2"/>
<rect x="16" y="16" width="12" height="12" rx="2.5" fill="#9c9440"/>
<rect x="30" y="16" width="12" height="12" rx="2.5" fill="#9c9440"/>
<rect x="45" y="15" width="12" height="12" rx="2.5" transform="rotate(12 51 21)" fill="#ffed00"/>
<rect x="30" y="30" width="12" height="12" rx="2.5" fill="#9c9440"/>
<rect x="30" y="44" width="12" height="12" rx="2.5" fill="#9c9440"/>
</g>
<text x="192" y="118" fill="#ffffff" font-family="'Segoe UI', Inter, 'Noto Sans', 'DejaVu Sans', sans-serif" font-size="60" font-weight="700" letter-spacing="-1">Tessera</text>
<rect width="1200" height="112" fill="#ffffff"/>
<!-- Flaechen von unten nach oben uebereinander, jede von oben bis zu ihrer
Wellenlinie: so teilen sich benachbarte Baender dieselbe Kante, ohne
Luecken dazwischen. -->
<path d="M0 0 V92 C220 78 420 102 600 96 S1000 76 1200 86 V0 Z" fill="#d9dce0"/>
<path d="M0 0 V70 C250 50 420 86 640 76 S1040 50 1200 64 V0 Z" fill="#2c3036"/>
<path d="M0 0 V44 C330 18 520 62 700 52 S1060 22 1200 40 V0 Z" fill="#1a1c20"/>
<path d="M0 44 C330 18 520 62 700 52 S1060 22 1200 40" fill="none" stroke="#ffed00" stroke-opacity="0.75" stroke-width="3"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

+3 -3
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env node
/**
* render-mail-header.mjs — erzeugt das Kopfbild der Willkommensmail
* (apps/api/assets/mail/welcome-header.png, 1200x300 fuer hochaufloesende
* Bildschirme, in der Mail 600x150 angezeigt) aus der daneben liegenden
* (apps/api/assets/mail/welcome-header.png, 1200x112 fuer hochaufloesende
* Bildschirme, in der Mail 600x56 angezeigt) aus der daneben liegenden
* Quelle welcome-header.svg.
*
* Warum ein PNG statt Inline-SVG oder CSS-Hintergrund: Outlook (Word-
@@ -33,7 +33,7 @@ const sharp = createRequire(nextPkg)('sharp');
const svg = readFileSync(join(assetDir, 'welcome-header.svg'));
const png = await sharp(svg, { density: 72 })
.resize(1200, 300)
.resize(1200, 112)
.png({ compressionLevel: 9, palette: false })
.toBuffer();
writeFileSync(join(assetDir, 'welcome-header.png'), png);
+55 -10
View File
@@ -11,10 +11,10 @@
* Mail sehr unterschiedlich darstellen:
* - Tabellenlayout, alle Stile inline, hoechstens 600 px breit;
* - keine externen Ressourcen, keine Web-Fonts (Systemschriften);
* - der Kopf im Stil des Dashboard-Hintergrunds "Duenen" ist ein PNG
* (`headerImageSrc`, im Versand `cid:`), weil SVG und CSS-Hintergruende in
* Outlook nicht erscheinen; fehlt das Bild, steht dort ein dunkler
* Textkopf;
* - Kopf: Bildmarke und Schriftzug als HTML (erscheinen immer), darunter
* die Duenen-Welle als schmaler PNG-Streifen (`headerImageSrc`, im
* Versand `cid:`), weil SVG und CSS-Hintergruende in Outlook nicht
* erscheinen; fehlt das Bild, bleibt nur ein schmaler Abschluss;
* - Knoepfe als Tabelle mit Hintergrundfarbe in der Zelle ("bulletproof"),
* Outlook ignoriert Innenabstaende und Rundungen am Link selbst.
*
@@ -118,13 +118,58 @@ function button(href: string, label: string, bg: string, fg: string): string {
</td></tr></table>`;
}
function headerRow(src: string | null): string {
if (src) {
return `<tr><td bgcolor="${C.header}" style="background-color:${C.header};border-radius:12px 12px 0 0;line-height:0;font-size:0;">
<img src="${escapeHtml(src)}" width="600" height="150" alt="Tessera" style="display:block;width:100%;max-width:600px;height:auto;border:0;outline:none;text-decoration:none;border-radius:12px 12px 0 0;color:#ffffff;font-family:${FONT};font-size:28px;font-weight:700;">
</td></tr>`;
/** Eine Kachel der Bildmarke: feste Zelle, Hoehe auch in Outlook exakt. */
function tile(color: string | null): string {
const bg = color ? `bgcolor="${color}" style="background-color:${color};` : 'style="';
return `<td width="9" height="9" ${bg}width:9px;height:9px;font-size:1px;line-height:9px;mso-line-height-rule:exactly;">&nbsp;</td>`;
}
return `<tr><td bgcolor="${C.header}" style="background-color:${C.header};border-radius:12px 12px 0 0;padding:40px 40px 36px;font-family:${FONT};font-size:30px;line-height:36px;font-weight:700;color:#ffffff;border-bottom:3px solid ${C.yellow};">Tessera</td></tr>`;
/** Luecke zwischen Kacheln. */
const GAP = '<td width="3" style="width:3px;font-size:1px;line-height:1px;">&nbsp;</td>';
/**
* Bildmarke als HTML (quick-260930): das Kachel-"T" aus Tabellenzellen —
* oben drei Kacheln (die dritte gelb, im Original gedreht), darunter zwei in
* der Mitte —, auf dunkler Grundplatte mit heller Kontur wie in der App.
* Braucht kein Bild und erscheint deshalb in jedem Mailprogramm.
*/
function logoMark(): string {
const row = (cells: Array<string | null>) =>
`<tr>${cells.map((c, i) => (i > 0 ? GAP : '') + tile(c)).join('')}</tr>`;
const spacer = `<tr><td colspan="5" height="3" style="height:3px;font-size:1px;line-height:3px;mso-line-height-rule:exactly;">&nbsp;</td></tr>`;
const olive = '#9c9440';
return `<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse:separate;">
<tr><td bgcolor="#111214" style="background-color:#111214;border:1px solid #3a3d44;border-radius:10px;padding:10px 10px 10px 10px;">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse:collapse;">
${row([olive, olive, C.yellow])}${spacer}${row([null, olive, null])}${spacer}${row([null, olive, null])}
</table>
</td></tr></table>`;
}
/**
* Kopf der Mail (quick-260930, Rueckmeldung des Nutzers: in Outlook "ein
* riesiger schwarzer Fleck, kein Logo"). Vorher steckten Logo und
* Schriftzug in EINEM 150 px hohen Bild; zeigt ein Mailprogramm das
* eingebettete Bild nicht an, blieb nur die dunkle Flaeche. Jetzt:
* - Bildmarke (HTML-Kacheln) und Schriftzug "Tessera" (echter Text) in einer
* niedrigen dunklen Leiste — erscheinen immer;
* - darunter die Duenen-Welle als schmaler Bildstreifen (`src`, im Versand
* `cid:`), der ins Weiss der Karte auslaeuft. Fehlt er, bleibt nur weisser
* Abstand, kein dunkler Block.
*/
function headerRow(src: string | null): string {
const bar = `<tr><td bgcolor="${C.header}" style="background-color:${C.header};border-radius:12px 12px 0 0;padding:22px 32px 14px;">
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr>
<td valign="middle" style="padding:0 14px 0 0;">${logoMark()}</td>
<td valign="middle" style="font-family:${FONT};font-size:26px;line-height:32px;font-weight:700;color:#ffffff;letter-spacing:-0.5px;">Tessera</td>
</tr></table>
</td></tr>`;
const wave = src
? `<tr><td bgcolor="${C.card}" style="background-color:${C.card};line-height:0;font-size:0;">
<img src="${escapeHtml(src)}" width="600" height="56" alt="" style="display:block;width:100%;max-width:600px;height:auto;border:0;outline:none;text-decoration:none;">
</td></tr>`
: `<tr><td bgcolor="${C.header}" height="4" style="background-color:${C.header};height:4px;font-size:1px;line-height:4px;border-bottom:3px solid ${C.yellow};">&nbsp;</td></tr>`;
return bar + wave;
}
export function renderWelcomeMail(input: WelcomeMailInput): RenderedWelcomeMail {