- <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>
213 lines
8.2 KiB
TypeScript
213 lines
8.2 KiB
TypeScript
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, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
|
||
/** 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 · 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> · <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} →</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"),
|
||
};
|
||
} |