- Replace img.rocket.new mock URLs in authors/[slug] + FeaturedBento with /assets/images/article-placeholder.svg (live DB data drives these pages now; the static maps are fallback only). - Replace broadcastb5322.builtwithrocket.new with broadcastbeat.com in admin API routes, news detail, forum layout, and the 4 Supabase edge functions that build outbound email links. - Swap rocket-hosted JSON-LD logo URL in root layout for the local /assets/images/logo.png we already ship. - Drop img.rocket.new from the Next.js image-hosts allowlist; add supabase.onsethost.com so storage proxy URLs can render. The DB has had zero rocket.new image refs for weeks; this finishes the job in the codebase so no page on broadcastbeat.com can render a rocket-hosted asset anymore.
165 lines
6.5 KiB
TypeScript
165 lines
6.5 KiB
TypeScript
import { serve } from "https://deno.land/std@0.192.0/http/server.ts";
|
|
|
|
serve(async (req) => {
|
|
// ✅ CORS preflight
|
|
if (req.method === "OPTIONS") {
|
|
return new Response("ok", {
|
|
headers: {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
"Access-Control-Allow-Headers": "*",
|
|
},
|
|
});
|
|
}
|
|
|
|
try {
|
|
const { article, subscribers } = await req.json();
|
|
|
|
if (!article || !article.title) {
|
|
return new Response(JSON.stringify({ error: "Article data is required" }), {
|
|
status: 400,
|
|
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
|
});
|
|
}
|
|
|
|
if (!subscribers || subscribers.length === 0) {
|
|
return new Response(JSON.stringify({ error: "No subscribers provided" }), {
|
|
status: 400,
|
|
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
|
});
|
|
}
|
|
|
|
const RESEND_API_KEY = Deno.env.get("RESEND_API_KEY");
|
|
if (!RESEND_API_KEY) {
|
|
throw new Error("RESEND_API_KEY is not set");
|
|
}
|
|
|
|
const siteUrl = "https://broadcastbeat.com";
|
|
const articleUrl = article.slug
|
|
? `${siteUrl}/articles/${article.slug}`
|
|
: article.wp_slug
|
|
? `${siteUrl}/articles/${article.wp_slug}`
|
|
: siteUrl;
|
|
|
|
const categoryLabel = article.category
|
|
? `<span style="display:inline-block;background-color:#1e3a5f;color:#3b82f6;font-size:11px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;padding:3px 10px;border-radius:2px;margin-bottom:16px;">${article.category}</span>`
|
|
: "";
|
|
|
|
const featuredImageBlock = article.featured_image
|
|
? `<img src="${article.featured_image}" alt="${article.featured_image_alt || article.title}" style="width:100%;max-width:600px;height:auto;display:block;margin-bottom:0;" />`
|
|
: "";
|
|
|
|
const excerptBlock = article.excerpt
|
|
? `<p style="margin:0 0 24px;font-size:15px;color:#aaaaaa;line-height:1.7;">${article.excerpt}</p>`
|
|
: "";
|
|
|
|
const authorBlock = article.author_name
|
|
? `<p style="margin:0 0 20px;font-size:12px;color:#666;text-transform:uppercase;letter-spacing:0.06em;">By ${article.author_name}</p>`
|
|
: "";
|
|
|
|
const htmlBody = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>${article.title} — BroadcastBeat</title>
|
|
</head>
|
|
<body style="margin:0;padding:0;background-color:#111111;font-family:Arial,Helvetica,sans-serif;">
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="background-color:#111111;padding:40px 20px;">
|
|
<tr>
|
|
<td align="center">
|
|
<table width="600" cellpadding="0" cellspacing="0" style="background-color:#1a1a1a;border:1px solid #2a2a2a;border-radius:4px;overflow:hidden;max-width:600px;width:100%;">
|
|
<!-- Header -->
|
|
<tr>
|
|
<td style="background-color:#1a2535;padding:20px 32px;border-bottom:1px solid #2a3a50;">
|
|
<a href="${siteUrl}" style="text-decoration:none;">
|
|
<p style="margin:0;font-family:Georgia,serif;font-size:20px;font-weight:bold;color:#e8e8e8;letter-spacing:0.02em;">BroadcastBeat</p>
|
|
<p style="margin:3px 0 0;font-size:10px;color:#888;letter-spacing:0.06em;text-transform:uppercase;">Digital Platform for Broadcast Engineering</p>
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
<!-- Featured Image -->
|
|
${featuredImageBlock ? `<tr><td style="padding:0;">${featuredImageBlock}</td></tr>` : ""}
|
|
<!-- Body -->
|
|
<tr>
|
|
<td style="padding:28px 32px 32px;">
|
|
<p style="margin:0 0 12px;font-size:11px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:#cc0000;">New Article</p>
|
|
${categoryLabel}
|
|
<h1 style="margin:0 0 12px;font-family:Georgia,serif;font-size:22px;color:#e8e8e8;line-height:1.35;">
|
|
<a href="${articleUrl}" style="color:#e8e8e8;text-decoration:none;">${article.title}</a>
|
|
</h1>
|
|
${authorBlock}
|
|
${excerptBlock}
|
|
<a href="${articleUrl}" style="display:inline-block;background-color:#cc0000;color:#ffffff;font-size:13px;font-weight:700;letter-spacing:0.04em;text-transform:uppercase;text-decoration:none;padding:11px 26px;border-radius:2px;">
|
|
Read Article →
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
<!-- Divider -->
|
|
<tr>
|
|
<td style="padding:0 32px;">
|
|
<div style="border-top:1px solid #2a2a2a;"></div>
|
|
</td>
|
|
</tr>
|
|
<!-- Footer -->
|
|
<tr>
|
|
<td style="background-color:#0d0d0d;padding:20px 32px;border-top:1px solid #1a1a1a;">
|
|
<p style="margin:0;font-size:11px;color:#555;line-height:1.6;">
|
|
You're receiving this because you subscribed to BroadcastBeat article notifications.
|
|
<br />
|
|
<a href="${siteUrl}" style="color:#3b82f6;text-decoration:none;">Visit BroadcastBeat</a>
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</body>
|
|
</html>`;
|
|
|
|
const results = { sent: 0, failed: 0, errors: [] as string[] };
|
|
|
|
// Send in batches of 50 to respect Resend rate limits
|
|
const batchSize = 50;
|
|
for (let i = 0; i < subscribers.length; i += batchSize) {
|
|
const batch = subscribers.slice(i, i + batchSize);
|
|
|
|
const response = await fetch("https://api.resend.com/emails", {
|
|
method: "POST",
|
|
headers: {
|
|
"Authorization": `Bearer ${RESEND_API_KEY}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
from: "onboarding@resend.dev",
|
|
to: batch,
|
|
subject: `New Article: ${article.title}`,
|
|
html: htmlBody,
|
|
}),
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (!response.ok) {
|
|
results.failed += batch.length;
|
|
results.errors.push(data.message || "Batch send failed");
|
|
} else {
|
|
results.sent += batch.length;
|
|
}
|
|
}
|
|
|
|
return new Response(
|
|
JSON.stringify({ success: true, sent: results.sent, failed: results.failed, errors: results.errors }),
|
|
{
|
|
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
|
}
|
|
);
|
|
} catch (error) {
|
|
return new Response(JSON.stringify({ error: error.message }), {
|
|
status: 500,
|
|
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
|
});
|
|
}
|
|
});
|