Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions app/api/cron/sponsor-outreach/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import { sanityWriteClient } from '@/lib/sanity-write-client'
import { generateOutreachEmail } from '@/lib/sponsor/gemini-outreach'
import { sendSponsorEmail } from '@/lib/sponsor/email-service'
import type { SponsorPoolEntry } from '@/lib/sponsor/gemini-outreach'

const MAX_PER_RUN = 5
const COOLDOWN_DAYS = 14
import { getConfig } from '@/lib/config'

export async function POST(request: Request) {
// Auth: Bearer token check against CRON_SECRET
Expand All @@ -25,9 +23,19 @@ export async function POST(request: Request) {
try {
console.log('[SPONSOR] Starting outbound sponsor outreach cron...')

// Fetch config from Sanity singleton
const sponsorCfg = await getConfig("sponsor_config");
const maxPerRun = sponsorCfg.maxOutreachPerRun;
const cooldownDays = sponsorCfg.cooldownDays;

// Build rate card string from config tiers
const rateCard = sponsorCfg.rateCardTiers
.map((t) => `- ${t.name} ($${t.price}) — ${t.description}`)
.join('\n');

// Calculate the cutoff date for cooldown
const cutoffDate = new Date()
cutoffDate.setDate(cutoffDate.getDate() - COOLDOWN_DAYS)
cutoffDate.setDate(cutoffDate.getDate() - cooldownDays)
const cutoffISO = cutoffDate.toISOString()

// Query Sanity for eligible sponsor pool entries
Expand All @@ -38,7 +46,7 @@ export async function POST(request: Request) {
!defined(lastContactedAt)
|| lastContactedAt < $cutoffDate
)
] | order(relevanceScore desc) [0...${MAX_PER_RUN - 1}] {
] | order(relevanceScore desc) [0...${maxPerRun - 1}] {
_id,
companyName,
contactName,
Expand Down Expand Up @@ -67,8 +75,8 @@ export async function POST(request: Request) {

for (const sponsor of sponsors) {
try {
// Generate personalized outreach email
const email = await generateOutreachEmail(sponsor)
// Generate personalized outreach email with config rate card
const email = await generateOutreachEmail(sponsor, rateCard)

// Send the email (stubbed)
const sendResult = await sendSponsorEmail(
Expand Down
8 changes: 7 additions & 1 deletion app/api/webhooks/sanity-distribute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { generateWithGemini } from "@/lib/gemini";
import { uploadVideo, uploadShort, generateShortsMetadata } from "@/lib/youtube-upload";
import { notifySubscribers } from "@/lib/resend-notify";
import { postVideoAnnouncement } from "@/lib/x-social";
import { getConfig } from "@/lib/config";

const WEBHOOK_SECRET = process.env.SANITY_WEBHOOK_SECRET;

Expand Down Expand Up @@ -140,6 +141,9 @@ async function appendDistributionLog(docId: string, entries: DistributionLogEntr
async function runDistribution(docId: string, doc: AutomatedVideoDoc): Promise<void> {
const log: DistributionLogEntry[] = [];

// Fetch distribution config from Sanity singleton
const distConfig = await getConfig("distribution_config");

try {
await updateStatus(docId, "uploading");

Expand Down Expand Up @@ -197,7 +201,7 @@ async function runDistribution(docId: string, doc: AutomatedVideoDoc): Promise<v
log.push(logEntry("youtube-short", "skipped", { error: "No shortUrl" }));
}

// Step 4: Email notification (non-fatal)
// Step 4: Email notification (non-fatal) — uses distribution config
console.log("[sanity-distribute] Step 4/6 - Sending email");
const ytUrl = youtubeVideoId ? `https://www.youtube.com/watch?v=${youtubeVideoId}` : doc.videoUrl || "";
try {
Expand All @@ -206,6 +210,8 @@ async function runDistribution(docId: string, doc: AutomatedVideoDoc): Promise<v
videoTitle: metadata.title,
videoUrl: ytUrl,
description: metadata.description.slice(0, 280),
fromEmail: distConfig.resendFromEmail,
notificationEmails: distConfig.notificationEmails,
});
log.push(logEntry("email", "success"));
} catch (e) {
Expand Down
6 changes: 4 additions & 2 deletions lib/resend-notify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export async function notifySubscribers(opts: {
videoTitle: string;
videoUrl: string;
description: string;
fromEmail?: string;
notificationEmails?: string[];
}): Promise<{ sent: boolean; error?: string }> {
const apiKey = process.env.RESEND_API_KEY;
if (!apiKey) {
Expand All @@ -19,8 +21,8 @@ export async function notifySubscribers(opts: {
const resend = new Resend(apiKey);

await resend.emails.send({
from: "CodingCat.dev <noreply@codingcat.dev>",
to: ["subscribers@codingcat.dev"], // TODO: fetch subscriber list
from: `CodingCat.dev <${opts.fromEmail || "noreply@codingcat.dev"}>`,
to: opts.notificationEmails || ["subscribers@codingcat.dev"],
subject: opts.subject,
html: `
<h1>${opts.videoTitle}</h1>
Expand Down
10 changes: 6 additions & 4 deletions lib/sponsor/gemini-outreach.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { GoogleGenerativeAI } from '@google/generative-ai'
import { getConfigValue } from '@/lib/config'

export interface SponsorPoolEntry {
_id: string
Expand All @@ -16,7 +17,7 @@ export interface OutreachEmail {
body: string
}

const RATE_CARD = `
const DEFAULT_RATE_CARD = `
CodingCat.dev Sponsorship Tiers:
- Dedicated Video ($4,000) — Full dedicated video about your product
- Integrated Mid-Roll Ad ($1,800) — Mid-roll advertisement in our videos
Expand All @@ -33,7 +34,7 @@ Our audience: 50K+ developers interested in web development, JavaScript/TypeScri
*/
export async function generateOutreachEmail(
sponsor: SponsorPoolEntry,
rateCard: string = RATE_CARD
rateCard: string = DEFAULT_RATE_CARD
): Promise<OutreachEmail> {
const apiKey = process.env.GEMINI_API_KEY
if (!apiKey) {
Expand All @@ -42,7 +43,8 @@ export async function generateOutreachEmail(
}

const genAI = new GoogleGenerativeAI(apiKey)
const model = genAI.getGenerativeModel({ model: process.env.GEMINI_MODEL || 'gemini-2.5-flash' })
const geminiModel = await getConfigValue("pipeline_config", "geminiModel", "gemini-2.0-flash");
const model = genAI.getGenerativeModel({ model: geminiModel })

const optOutUrl = sponsor.optOutToken
? `${process.env.NEXT_PUBLIC_URL || 'https://codingcat.dev'}/api/sponsor/opt-out?token=${sponsor.optOutToken}`
Expand Down Expand Up @@ -76,7 +78,7 @@ Respond ONLY with valid JSON, no markdown formatting:
const response = result.response
const text = response.text().trim()

const jsonStr = text.replace(/^\`\`\`json?\n?/i, '').replace(/\n?\`\`\`$/i, '').trim()
const jsonStr = text.replace(/^```json?\n?/i, '').replace(/\n?```$/i, '').trim()
const parsed = JSON.parse(jsonStr) as OutreachEmail

console.log('[SPONSOR] Gemini outreach email generated for:', sponsor.companyName)
Expand Down
Loading