Files
kolaytercih/src/lib/eposta.ts
bilalgursen aac33e7533 chore(eposta): şablondan harici font + footer iletişim
- <head>'deki Google Fonts preconnect/stylesheet <link>'leri kaldırıldı
  (çoğu istemci yok sayıyor, bazı spam filtrelerinde eksi puan).
- HTML footer'a ve düz metin gövdelerin sonuna kolaytercih.com +
  destek@kolaytercih.com satırı eklendi. Satış dili yok.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 04:47:30 +03:00

213 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import "server-only";
/**
* E-posta şablonları.
*
* Kurallar: mail istemcileri harici CSS, <style> kuralı, flexbox ve SVG
* desteğini güvenilir biçimde sunmaz — bu yüzden düzen tablo tabanlı ve tüm
* stiller satır içi. Logo işareti sitedeki SVG'nin raster kopyası olan
* `/kolay-tercih-mark.png` ile gösterilir (SVG'yi Gmail/Outlook engeller).
* Harici font <link>'i yok: çoğu istemci yok sayıyor, bazı spam filtreleri
* harici kaynağı eksi puan sayıyor. Cal Sans / Bricolage Grotesque yalnızca
* cihazda kuruluysa görünür; aksi hâlde sistem yazı tipine düşer.
*/
import { SITE_URL } from "./seo";
const MAVI = "#3b82f6";
const LACIVERT = "#0f172b";
const GRI = "#475569";
const KENAR = "#e2e8f0";
const ZEMIN = "#f8fafc";
const YAZI_TIPI =
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
/** Sitedeki başlıklar Cal Sans (--font-heading); destekleyen istemcide o görünür. */
const BASLIK_TIPI = `'Cal Sans', ${YAZI_TIPI}`;
/** Sitedeki kelime markası Bricolage Grotesque (font-bricolage). */
const MARKA_TIPI = `'Bricolage Grotesque', ${YAZI_TIPI}`;
/** Alt bilgideki iletişim satırı — HTML footer ve düz metin gövdeler ortak kullanır. */
const SITE_ALANI = "kolaytercih.com";
const DESTEK_EPOSTA = "destek@kolaytercih.com";
const METIN_ALT_BILGI = ["", "—", `${SITE_ALANI} · ${DESTEK_EPOSTA}`];
export type EpostaIcerik = {
subject: string;
html: string;
text: string;
};
/** HTML bağlamına gömülen değerleri kaçırır — token'lı URL'lerdeki & şart. */
function kacir(deger: string): string {
return deger
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
/** Logo: sitedeki işaretin raster kopyası + KolayTercih kelime markası. */
function baslik(): string {
return `
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="padding-right:6px;">
<img src="${SITE_URL}/kolay-tercih-mark.png" width="36" height="36" alt="" style="display:block;width:36px;height:36px;border:0;">
</td>
<td style="font-family:${MARKA_TIPI};font-size:22px;letter-spacing:-0.05em;color:${LACIVERT};">
<span style="font-weight:300;">Kolay</span><span style="font-weight:600;">Tercih</span>
</td>
</tr>
</table>`;
}
/**
* Tüm e-postaların ortak kabuğu: dış zemin, beyaz kart, logo ve alt bilgi.
* `onizleme` gelen kutusunda konunun yanında görünen gizli özet metindir.
*/
function kabuk({
onizleme,
govde,
baslikEtiketi,
}: {
onizleme: string;
govde: string;
/** <title> içeriği — genelde konuyla aynı. */
baslikEtiketi: string;
}): string {
return `<!doctype html>
<html lang="tr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="light">
<title>${kacir(baslikEtiketi)}</title>
</head>
<body style="margin:0;padding:0;background-color:${ZEMIN};">
<div style="display:none;max-height:0;overflow:hidden;opacity:0;color:transparent;font-size:1px;line-height:1px;">${onizleme}</div>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background-color:${ZEMIN};">
<tr>
<td align="center" style="padding:32px 16px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="max-width:520px;">
<tr>
<td style="padding-bottom:20px;">${baslik()}</td>
</tr>
<tr>
<td style="background-color:#ffffff;border:1px solid ${KENAR};border-radius:16px;padding:32px;font-family:${YAZI_TIPI};font-size:15px;line-height:1.65;color:${GRI};">
${govde}
</td>
</tr>
<tr>
<td style="padding:20px 8px 0;font-family:${YAZI_TIPI};font-size:12px;line-height:1.6;color:#94a3b8;">
KolayTercih &middot; YKS tercih listeni dakikalar içinde hazırla<br>
Bu e-postayı hesabınla ilgili bir işlem yaptığın için aldın.<br>
<a href="https://${SITE_ALANI}" style="color:#94a3b8;">${SITE_ALANI}</a> &middot; <a href="mailto:${DESTEK_EPOSTA}" style="color:#94a3b8;">${DESTEK_EPOSTA}</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>`;
}
/** Ortalanmış, tablo tabanlı CTA butonu (Outlook'ta da zemin rengi tutar). */
function buton(url: string, etiket: string): string {
return `
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr>
<td align="center" style="background-color:${MAVI};border-radius:10px;">
<a href="${url}" style="display:inline-block;padding:14px 28px;font-family:${YAZI_TIPI};font-size:15px;font-weight:600;color:#ffffff;text-decoration:none;">${etiket} &rarr;</a>
</td>
</tr>
</table>`;
}
export function magicLinkEpostasi(url: string): EpostaIcerik {
const guvenliUrl = kacir(url);
const govde = `
<h1 style="margin:0 0 12px;font-family:${BASLIK_TIPI};font-size:22px;line-height:1.35;font-weight:700;color:${LACIVERT};">
Merhaba, giriş bağlantını gönderdik
</h1>
<p style="margin:0 0 24px;">
Aşağıdaki butona tıklayarak KolayTercih hesabına giriş yapabilirsin.
Bağlantı <strong style="color:${LACIVERT};">15 dakika</strong> geçerli ve
yalnızca bir kez kullanılabilir.
</p>
${buton(guvenliUrl, "Giriş yap")}
<p style="margin:24px 0 0;font-size:13px;">
Buton çalışmazsa bu adresi tarayıcının adres çubuğuna yapıştır:<br>
<a href="${guvenliUrl}" style="color:${MAVI};word-break:break-all;">${guvenliUrl}</a>
</p>
<div style="margin:24px 0 0;padding-top:20px;border-top:1px solid ${KENAR};font-size:13px;color:#94a3b8;">
Bu isteği sen yapmadıysan bu e-postayı yok sayabilirsin; bağlantıya
tıklanmadığı sürece hesabında hiçbir şey değişmez.
</div>`;
return {
subject: "Merhaba, giriş bağlantını gönderdik",
html: kabuk({
onizleme: "Giriş bağlantın 15 dakika geçerli.",
govde,
baslikEtiketi: "Merhaba, giriş bağlantını gönderdik",
}),
text: [
"Merhaba, giriş bağlantını gönderdik",
"",
"Hesabına giriş yapmak için bu adresi tarayıcında aç:",
url,
"",
"Bağlantı 15 dakika geçerli ve yalnızca bir kez kullanılabilir.",
"Bu isteği sen yapmadıysan bu e-postayı yok sayabilirsin.",
...METIN_ALT_BILGI,
].join("\n"),
};
}
/**
* Kredi bittikten 48 saat sonra giden tek seferlik hatırlatma. KVKK sınıfı
* netleşene dek yalnızca işlemsel dil: hesaptaki mevcut listeyi hatırlatır,
* ödeme ancak ikincil bir cümlede geçer (bkz. PRD İP-A / A2).
*/
export function krediHatirlatmaEpostasi(): EpostaIcerik {
const listemUrl = `${SITE_URL}/listem`;
const konu = "Listen seni bekliyor — açık satırların hazır";
const govde = `
<h1 style="margin:0 0 12px;font-family:${BASLIK_TIPI};font-size:22px;line-height:1.35;font-weight:700;color:${LACIVERT};">
Listen seni bekliyor
</h1>
<p style="margin:0 0 24px;">
Yapay zekânın senin sıralamana göre kurduğu tercih listesi hesabında
duruyor; gerekçeleri ve risk notlarıyla
<strong style="color:${LACIVERT};">açık satırların hazır</strong>.
Kaldığın yerden devam edebilirsin.
</p>
${buton(listemUrl, "Listeni gör")}
<p style="margin:24px 0 0;font-size:13px;">
Listenin tamamını açmak ya da danışmana soru sormak istersen kredi
yükleyebilirsin — listen ve sohbet geçmişin olduğu gibi durur.
</p>`;
return {
subject: konu,
html: kabuk({
onizleme: "Yapay zekânın kurduğu listen hesabında duruyor.",
govde,
baslikEtiketi: konu,
}),
text: [
"Listen seni bekliyor",
"",
"Yapay zekânın senin sıralamana göre kurduğu tercih listesi hesabında",
"duruyor; gerekçeleri ve risk notlarıyla açık satırların hazır.",
"",
"Listeni görmek için:",
listemUrl,
"",
"Listenin tamamını açmak ya da danışmana soru sormak istersen kredi",
"yükleyebilirsin — listen ve sohbet geçmişin olduğu gibi durur.",
...METIN_ALT_BILGI,
].join("\n"),
};
}