Initial release v1.0.0
Open-source marketplace for AI Agent skills. Features: - Next.js 15 web app with i18n (en/fa) - CLI tool for skill installation (npx skillhub) - GitHub crawler/indexer with multi-strategy discovery - Security scanning for all indexed skills - Self-hostable with Docker Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
438
apps/web/lib/email-templates.ts
Normal file
438
apps/web/lib/email-templates.ts
Normal file
@@ -0,0 +1,438 @@
|
||||
type Locale = 'en' | 'fa';
|
||||
|
||||
const SITE_URL = process.env.NEXT_PUBLIC_APP_URL || 'https://skills.palebluedot.live';
|
||||
|
||||
const COLORS = {
|
||||
primary: '#0284c7',
|
||||
primaryDark: '#0369a1',
|
||||
background: '#f8fafc',
|
||||
surface: '#ffffff',
|
||||
border: '#e2e8f0',
|
||||
text: '#1e293b',
|
||||
textSecondary: '#64748b',
|
||||
textMuted: '#94a3b8',
|
||||
};
|
||||
|
||||
function getDir(locale: Locale): 'rtl' | 'ltr' {
|
||||
return locale === 'fa' ? 'rtl' : 'ltr';
|
||||
}
|
||||
|
||||
function getAlign(locale: Locale): 'right' | 'left' {
|
||||
return locale === 'fa' ? 'right' : 'left';
|
||||
}
|
||||
|
||||
function getFontFamily(locale: Locale): string {
|
||||
return locale === 'fa'
|
||||
? 'Tahoma, Arial, sans-serif'
|
||||
: 'Arial, Helvetica, sans-serif';
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared base layout wrapper for all email templates
|
||||
*/
|
||||
function baseLayout(locale: Locale, bodyContent: string, footerContent: string): string {
|
||||
const dir = getDir(locale);
|
||||
const align = getAlign(locale);
|
||||
const font = getFontFamily(locale);
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="${locale}" dir="${dir}">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SkillHub</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: ${COLORS.background}; font-family: ${font}; direction: ${dir};">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color: ${COLORS.background};">
|
||||
<tr>
|
||||
<td align="center" style="padding: 32px 16px;">
|
||||
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width: 600px; width: 100%;">
|
||||
<!-- Header -->
|
||||
<tr>
|
||||
<td style="background-color: ${COLORS.primary}; padding: 24px 32px; border-radius: 12px 12px 0 0;">
|
||||
<a href="${SITE_URL}" style="color: #ffffff; text-decoration: none; font-size: 24px; font-weight: bold; font-family: ${font};">
|
||||
SkillHub
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Body -->
|
||||
<tr>
|
||||
<td style="background-color: ${COLORS.surface}; padding: 32px; text-align: ${align}; font-family: ${font};">
|
||||
${bodyContent}
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="background-color: ${COLORS.background}; padding: 24px 32px; border-top: 1px solid ${COLORS.border}; border-radius: 0 0 12px 12px; text-align: center;">
|
||||
${footerContent}
|
||||
<p style="margin: 12px 0 0 0; color: ${COLORS.textMuted}; font-size: 12px; font-family: ${font};">
|
||||
© ${new Date().getFullYear()} SkillHub —
|
||||
<a href="${SITE_URL}" style="color: ${COLORS.textMuted}; text-decoration: underline;">${new URL(SITE_URL).hostname}</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function ctaButton(locale: Locale, text: string, href: string): string {
|
||||
const font = getFontFamily(locale);
|
||||
return `<a href="${href}" style="display: inline-block; background-color: ${COLORS.primary}; color: #ffffff; padding: 12px 28px; border-radius: 8px; text-decoration: none; font-weight: bold; font-size: 14px; font-family: ${font}; margin: 4px;">${text}</a>`;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Translation strings
|
||||
// ============================================================================
|
||||
|
||||
const translations = {
|
||||
en: {
|
||||
welcome: {
|
||||
subject: 'Welcome to SkillHub!',
|
||||
greeting: (username: string) => `Hi ${username},`,
|
||||
intro: 'Welcome to SkillHub — the open-source marketplace where AI agents find the expertise they need.',
|
||||
whatIs: (count: string) => `Browse ${count}+ skills that work with Claude Code, GitHub Copilot, OpenAI Codex, Cursor, and Windsurf.`,
|
||||
getStarted: 'Get started:',
|
||||
browseSkills: 'Browse Skills',
|
||||
installCli: 'Install CLI',
|
||||
docs: 'Documentation',
|
||||
newsletterCta: 'Want to stay updated? Subscribe to our newsletter for new skills, features, and tips.',
|
||||
subscribeLink: 'Subscribe to Newsletter',
|
||||
footer: 'You received this because you signed up on SkillHub with GitHub.',
|
||||
},
|
||||
newsletter: {
|
||||
subject: 'You\'re in — welcome to SkillHub Newsletter',
|
||||
greeting: 'Thanks for subscribing!',
|
||||
intro: 'You\'ll get concise updates on what matters:',
|
||||
item1: 'New and trending skills in the marketplace',
|
||||
item2: 'Feature releases and platform improvements',
|
||||
item3: 'Practical tips for AI-powered development',
|
||||
visitUs: 'Visit SkillHub',
|
||||
footer: 'You received this because you subscribed to the SkillHub newsletter.',
|
||||
unsubscribe: 'Unsubscribe',
|
||||
},
|
||||
claimSubmitted: {
|
||||
addSubject: 'Your skill request has been submitted',
|
||||
removeSubject: 'Your removal request has been processed',
|
||||
addGreeting: 'Your request has been submitted!',
|
||||
removeGreeting: 'Your removal request has been processed!',
|
||||
addIntro: (repoUrl: string, count: number) =>
|
||||
`We received your request to add <strong>${repoUrl}</strong>${count > 0 ? ` (${count} skill${count > 1 ? 's' : ''} found)` : ''}.`,
|
||||
removeIntro: (skillId: string) =>
|
||||
`Your request to remove <strong>${skillId}</strong> has been verified and processed.`,
|
||||
addPending: 'Your request is now pending review. We\'ll notify you when it\'s been processed.',
|
||||
removeApproved: 'The skill has been blocked from indexing and will no longer appear on SkillHub.',
|
||||
viewRequests: 'View My Requests',
|
||||
footer: 'You received this because you submitted a request on SkillHub.',
|
||||
},
|
||||
claimStatus: {
|
||||
approvedSubject: 'Your request has been approved',
|
||||
rejectedSubject: 'Update on your request',
|
||||
approvedGreeting: 'Good news!',
|
||||
rejectedGreeting: 'Update on your request',
|
||||
addApproved: (repoUrl: string) =>
|
||||
`Your request to add <strong>${repoUrl}</strong> has been approved and will be indexed soon.`,
|
||||
addRejected: (repoUrl: string) =>
|
||||
`After review, your request to add <strong>${repoUrl}</strong> could not be approved at this time.`,
|
||||
removeApproved: (skillId: string) =>
|
||||
`Your request to remove <strong>${skillId}</strong> has been approved. The skill has been blocked from indexing.`,
|
||||
removeRejected: (skillId: string) =>
|
||||
`After review, your request to remove <strong>${skillId}</strong> could not be approved at this time.`,
|
||||
viewRequests: 'View My Requests',
|
||||
footer: 'You received this because you submitted a request on SkillHub.',
|
||||
},
|
||||
},
|
||||
fa: {
|
||||
welcome: {
|
||||
subject: 'به SkillHub خوش آمدید!',
|
||||
greeting: (username: string) => `سلام ${username}،`,
|
||||
intro: 'به SkillHub خوش آمدید — بازار متنباز مهارتهایی که عاملهای هوش مصنوعی به آن نیاز دارند.',
|
||||
whatIs: (count: string) => `بیش از ${count} مهارت را مرور کنید که با Claude Code، GitHub Copilot، OpenAI Codex، Cursor و Windsurf کار میکنند.`,
|
||||
getStarted: 'شروع کنید:',
|
||||
browseSkills: 'مرور مهارتها',
|
||||
installCli: 'نصب CLI',
|
||||
docs: 'مستندات',
|
||||
newsletterCta: 'میخواهید بهروز بمانید؟ در خبرنامه ما برای مهارتها، ویژگیها و نکات جدید عضو شوید.',
|
||||
subscribeLink: 'عضویت در خبرنامه',
|
||||
footer: 'این ایمیل به دلیل ثبتنام شما در SkillHub با GitHub ارسال شده است.',
|
||||
},
|
||||
newsletter: {
|
||||
subject: 'عضویت شما فعال شد — خبرنامه SkillHub',
|
||||
greeting: 'ممنون از عضویت شما!',
|
||||
intro: 'بهروزرسانیهای مختصر و مفید دریافت خواهید کرد:',
|
||||
item1: 'مهارتهای جدید و پرطرفدار در بازار',
|
||||
item2: 'ویژگیهای جدید و بهبودهای پلتفرم',
|
||||
item3: 'نکات عملی برای توسعه با هوش مصنوعی',
|
||||
visitUs: 'مشاهده SkillHub',
|
||||
footer: 'این ایمیل به دلیل عضویت شما در خبرنامه SkillHub ارسال شده است.',
|
||||
unsubscribe: 'لغو عضویت',
|
||||
},
|
||||
claimSubmitted: {
|
||||
addSubject: 'درخواست افزودن مهارت شما ثبت شد',
|
||||
removeSubject: 'درخواست حذف مهارت شما پردازش شد',
|
||||
addGreeting: 'درخواست شما ثبت شد!',
|
||||
removeGreeting: 'درخواست حذف شما پردازش شد!',
|
||||
addIntro: (repoUrl: string, count: number) =>
|
||||
`درخواست افزودن <strong>${repoUrl}</strong>${count > 0 ? ` (${count} مهارت یافت شد)` : ''} دریافت شد.`,
|
||||
removeIntro: (skillId: string) =>
|
||||
`درخواست حذف <strong>${skillId}</strong> تایید و پردازش شد.`,
|
||||
addPending: 'درخواست شما در انتظار بررسی است. پس از پردازش به شما اطلاع خواهیم داد.',
|
||||
removeApproved: 'مهارت از ایندکس شدن مسدود شد و دیگر در SkillHub نمایش داده نخواهد شد.',
|
||||
viewRequests: 'مشاهده درخواستها',
|
||||
footer: 'این ایمیل به دلیل ثبت درخواست شما در SkillHub ارسال شده است.',
|
||||
},
|
||||
claimStatus: {
|
||||
approvedSubject: 'درخواست شما تایید شد',
|
||||
rejectedSubject: 'بهروزرسانی درخواست شما',
|
||||
approvedGreeting: 'خبر خوب!',
|
||||
rejectedGreeting: 'بهروزرسانی درخواست شما',
|
||||
addApproved: (repoUrl: string) =>
|
||||
`درخواست افزودن <strong>${repoUrl}</strong> تایید شد و به زودی ایندکس خواهد شد.`,
|
||||
addRejected: (repoUrl: string) =>
|
||||
`پس از بررسی، درخواست افزودن <strong>${repoUrl}</strong> در حال حاضر قابل تایید نیست.`,
|
||||
removeApproved: (skillId: string) =>
|
||||
`درخواست حذف <strong>${skillId}</strong> تایید شد. مهارت از ایندکس شدن مسدود شد.`,
|
||||
removeRejected: (skillId: string) =>
|
||||
`پس از بررسی، درخواست حذف <strong>${skillId}</strong> در حال حاضر قابل تایید نیست.`,
|
||||
viewRequests: 'مشاهده درخواستها',
|
||||
footer: 'این ایمیل به دلیل ثبت درخواست شما در SkillHub ارسال شده است.',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
// Template Builders
|
||||
// ============================================================================
|
||||
|
||||
export interface EmailTemplate {
|
||||
subject: string;
|
||||
html: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Welcome email for new users (first GitHub OAuth login)
|
||||
*/
|
||||
export function buildWelcomeEmail(locale: Locale, username: string, email?: string, totalSkillCount?: number): EmailTemplate {
|
||||
const t = translations[locale].welcome;
|
||||
const dir = getDir(locale);
|
||||
const align = getAlign(locale);
|
||||
|
||||
// Format skill count: dynamic if provided, fallback to "172,000+"
|
||||
const skillCountStr = totalSkillCount
|
||||
? (locale === 'fa'
|
||||
? totalSkillCount.toLocaleString('fa-IR')
|
||||
: totalSkillCount.toLocaleString('en-US'))
|
||||
: (locale === 'fa' ? '۱۷۲,۰۰۰' : '172,000');
|
||||
|
||||
const body = `
|
||||
<h1 style="margin: 0 0 16px 0; color: ${COLORS.text}; font-size: 24px; text-align: ${align};">
|
||||
${t.greeting(username)}
|
||||
</h1>
|
||||
<p style="margin: 0 0 12px 0; color: ${COLORS.text}; font-size: 15px; line-height: 1.6;">
|
||||
${t.intro}
|
||||
</p>
|
||||
<p style="margin: 0 0 20px 0; color: ${COLORS.textSecondary}; font-size: 14px; line-height: 1.6;">
|
||||
${t.whatIs(skillCountStr)}
|
||||
</p>
|
||||
<p style="margin: 0 0 12px 0; color: ${COLORS.text}; font-size: 15px; font-weight: bold;">
|
||||
${t.getStarted}
|
||||
</p>
|
||||
<div style="text-align: center; margin: 20px 0;">
|
||||
${ctaButton(locale, t.browseSkills, `${SITE_URL}/${locale}/browse`)}
|
||||
${ctaButton(locale, t.docs, `${SITE_URL}/${locale}/docs`)}
|
||||
</div>
|
||||
<p style="margin: 0 0 4px 0; color: ${COLORS.textSecondary}; font-size: 13px;">
|
||||
${locale === 'en' ? 'Install CLI:' : 'نصب CLI:'}
|
||||
</p>
|
||||
<div style="background-color: ${COLORS.background}; border: 1px solid ${COLORS.border}; border-radius: 6px; padding: 10px 14px; margin: 0 0 16px 0;" dir="ltr">
|
||||
<code style="font-family: 'Courier New', monospace; font-size: 13px; color: ${COLORS.text};">npm install -g skillhub</code>
|
||||
</div>
|
||||
<div style="border-left: 3px solid ${COLORS.primary}; padding: 0 0 0 14px; margin: 0 0 24px 0;">
|
||||
<p style="margin: 0 0 4px 0; color: ${COLORS.text}; font-size: 13px; font-weight: bold;">
|
||||
${locale === 'en' ? '💡 Tip' : '💡 نکته'}
|
||||
</p>
|
||||
<p style="margin: 0 0 6px 0; color: ${COLORS.textSecondary}; font-size: 12px; line-height: 1.5;">
|
||||
${locale === 'en'
|
||||
? 'Skills work best when explicitly invoked. Try searching and installing on the fly:'
|
||||
: 'مهارت\u200Cها وقتی بهترین عملکرد را دارند که صریحاً فراخوانی شوند. جستجو و نصب لحظه\u200Cای را امتحان کنید:'}
|
||||
</p>
|
||||
<div style="background-color: #1e293b; border-radius: 4px; padding: 8px 12px; margin: 0;" dir="ltr">
|
||||
<code style="font-family: 'Courier New', monospace; font-size: 11px; color: #e2e8f0; line-height: 1.7;">
|
||||
npx skillhub search "react testing" --sort stars<br/>
|
||||
npx skillhub install <skill-id> --project
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<div style="border-top: 1px solid ${COLORS.border}; padding-top: 20px; margin-top: 8px;">
|
||||
<p style="margin: 0 0 12px 0; color: ${COLORS.textSecondary}; font-size: 14px; line-height: 1.6;">
|
||||
${t.newsletterCta}
|
||||
</p>
|
||||
<div style="text-align: ${dir === 'rtl' ? 'right' : 'left'};">
|
||||
<a href="${SITE_URL}/api/newsletter/subscribe?email=${email ? encodeURIComponent(email) : ''}&locale=${locale}" style="color: ${COLORS.primary}; text-decoration: underline; font-size: 14px;">
|
||||
${t.subscribeLink} →
|
||||
</a>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const footer = `
|
||||
<p style="margin: 0; color: ${COLORS.textMuted}; font-size: 12px;">
|
||||
${t.footer}
|
||||
</p>`;
|
||||
|
||||
return {
|
||||
subject: t.subject,
|
||||
html: baseLayout(locale, body, footer),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Newsletter welcome email for actual newsletter subscribers
|
||||
*/
|
||||
export function buildNewsletterWelcomeEmail(locale: Locale, email: string): EmailTemplate {
|
||||
const t = translations[locale].newsletter;
|
||||
const align = getAlign(locale);
|
||||
|
||||
const body = `
|
||||
<h1 style="margin: 0 0 16px 0; color: ${COLORS.text}; font-size: 24px; text-align: ${align};">
|
||||
${t.greeting}
|
||||
</h1>
|
||||
<p style="margin: 0 0 16px 0; color: ${COLORS.text}; font-size: 15px; line-height: 1.6;">
|
||||
${t.intro}
|
||||
</p>
|
||||
<ul style="margin: 0 0 24px 0; padding: 0 0 0 ${locale === 'fa' ? '0' : '20px'}; ${locale === 'fa' ? 'padding-right: 20px; list-style-position: inside;' : ''} color: ${COLORS.textSecondary}; font-size: 14px; line-height: 2;">
|
||||
<li>${t.item1}</li>
|
||||
<li>${t.item2}</li>
|
||||
<li>${t.item3}</li>
|
||||
</ul>
|
||||
<div style="background-color: ${COLORS.background}; border-radius: 6px; padding: 12px 16px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0; color: ${COLORS.textSecondary}; font-size: 13px; line-height: 1.6;">
|
||||
💡 ${locale === 'en'
|
||||
? `<strong>Tip:</strong> Skills work best when explicitly invoked. Try searching and installing on the fly: <code style="background: ${COLORS.surface}; padding: 1px 4px; border-radius: 3px; font-size: 12px;">npx skillhub search "your topic"</code>. After installing, read the SKILL.md and follow its instructions. <a href="${SITE_URL}/${locale}/docs/cli" style="color: ${COLORS.primary}; text-decoration: none;">See CLI docs →</a>`
|
||||
: `<strong>نکته:</strong> مهارت\u200Cها وقتی بهترین عملکرد را دارند که صریحاً فراخوانی شوند. جستجو و نصب لحظه\u200Cای را امتحان کنید: <code style="background: ${COLORS.surface}; padding: 1px 4px; border-radius: 3px; font-size: 12px;" dir="ltr">npx skillhub search "your topic"</code>. پس از نصب، فایل SKILL.md را بخوانید و دستورالعمل\u200Cها را دنبال کنید. <a href="${SITE_URL}/${locale}/docs/cli" style="color: ${COLORS.primary}; text-decoration: none;">مستندات CLI →</a>`}
|
||||
</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin: 24px 0;">
|
||||
${ctaButton(locale, t.visitUs, `${SITE_URL}/${locale}`)}
|
||||
</div>`;
|
||||
|
||||
const unsubscribeUrl = `${SITE_URL}/api/newsletter/unsubscribe?email=${encodeURIComponent(email)}`;
|
||||
const footer = `
|
||||
<p style="margin: 0; color: ${COLORS.textMuted}; font-size: 12px;">
|
||||
${t.footer}
|
||||
<br />
|
||||
<a href="${unsubscribeUrl}" style="color: ${COLORS.textMuted}; text-decoration: underline;">${t.unsubscribe}</a>
|
||||
</p>`;
|
||||
|
||||
return {
|
||||
subject: t.subject,
|
||||
html: baseLayout(locale, body, footer),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim submission confirmation email
|
||||
*/
|
||||
export function buildClaimSubmittedEmail(
|
||||
locale: Locale,
|
||||
type: 'add' | 'remove',
|
||||
details: { skillId?: string; repositoryUrl?: string; skillCount?: number }
|
||||
): EmailTemplate {
|
||||
const t = translations[locale].claimSubmitted;
|
||||
const align = getAlign(locale);
|
||||
|
||||
const subject = type === 'add' ? t.addSubject : t.removeSubject;
|
||||
const greeting = type === 'add' ? t.addGreeting : t.removeGreeting;
|
||||
|
||||
let introText: string;
|
||||
let statusText: string;
|
||||
|
||||
if (type === 'add') {
|
||||
introText = t.addIntro(details.repositoryUrl || '', details.skillCount || 0);
|
||||
statusText = t.addPending;
|
||||
} else {
|
||||
introText = t.removeIntro(details.skillId || '');
|
||||
statusText = t.removeApproved;
|
||||
}
|
||||
|
||||
const body = `
|
||||
<h1 style="margin: 0 0 16px 0; color: ${COLORS.text}; font-size: 24px; text-align: ${align};">
|
||||
${greeting}
|
||||
</h1>
|
||||
<p style="margin: 0 0 12px 0; color: ${COLORS.text}; font-size: 15px; line-height: 1.6;">
|
||||
${introText}
|
||||
</p>
|
||||
<p style="margin: 0 0 24px 0; color: ${COLORS.textSecondary}; font-size: 14px; line-height: 1.6;">
|
||||
${statusText}
|
||||
</p>
|
||||
<div style="text-align: center; margin: 24px 0;">
|
||||
${ctaButton(locale, t.viewRequests, `${SITE_URL}/${locale}/claim`)}
|
||||
</div>`;
|
||||
|
||||
const footer = `
|
||||
<p style="margin: 0; color: ${COLORS.textMuted}; font-size: 12px;">
|
||||
${t.footer}
|
||||
</p>`;
|
||||
|
||||
return {
|
||||
subject,
|
||||
html: baseLayout(locale, body, footer),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim status change notification email
|
||||
*/
|
||||
export function buildClaimStatusEmail(
|
||||
locale: Locale,
|
||||
type: 'add' | 'remove',
|
||||
status: 'approved' | 'rejected',
|
||||
details: { skillId?: string; repositoryUrl?: string }
|
||||
): EmailTemplate {
|
||||
const t = translations[locale].claimStatus;
|
||||
const align = getAlign(locale);
|
||||
|
||||
const subject = status === 'approved' ? t.approvedSubject : t.rejectedSubject;
|
||||
const greeting = status === 'approved' ? t.approvedGreeting : t.rejectedGreeting;
|
||||
|
||||
let messageText: string;
|
||||
if (type === 'add' && status === 'approved') {
|
||||
messageText = t.addApproved(details.repositoryUrl || '');
|
||||
} else if (type === 'add' && status === 'rejected') {
|
||||
messageText = t.addRejected(details.repositoryUrl || '');
|
||||
} else if (type === 'remove' && status === 'approved') {
|
||||
messageText = t.removeApproved(details.skillId || '');
|
||||
} else {
|
||||
messageText = t.removeRejected(details.skillId || '');
|
||||
}
|
||||
|
||||
const body = `
|
||||
<h1 style="margin: 0 0 16px 0; color: ${COLORS.text}; font-size: 24px; text-align: ${align};">
|
||||
${greeting}
|
||||
</h1>
|
||||
<p style="margin: 0 0 24px 0; color: ${COLORS.text}; font-size: 15px; line-height: 1.6;">
|
||||
${messageText}
|
||||
</p>
|
||||
<div style="text-align: center; margin: 24px 0;">
|
||||
${ctaButton(locale, t.viewRequests, `${SITE_URL}/${locale}/claim`)}
|
||||
</div>`;
|
||||
|
||||
const footer = `
|
||||
<p style="margin: 0; color: ${COLORS.textMuted}; font-size: 12px;">
|
||||
${t.footer}
|
||||
</p>`;
|
||||
|
||||
return {
|
||||
subject,
|
||||
html: baseLayout(locale, body, footer),
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Outreach Email Template (English only — for GitHub repo owners)
|
||||
// ============================================================================
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user