av: clone broadcastbeat sources + AV Beat rebrand (Phase 1)

- Replaces the prior Rocket scaffold (saved on pre-bb-clone-rollback branch)
- Domain: broadcastbeat.com -> avbeat.com
- Brand: Broadcast Beat -> AV Beat
- Slug: broadcastbeat -> avbeat (package, identifiers)
- Schema fallback: bb -> av (env var name unchanged)
- Placeholder AV BEAT logo (gold->red gradient) at /assets/logos/av.svg
- All BB/RMP logo references repointed

Outstanding before public DNS swap:
  - Supabase av schema bootstrap (mirror of bb tables)
  - WordPress archive import (avbeat-com -> av.articles)
  - Coolify env vars
  - dev-avbeat.onsethost.com staging deploy + visual review
This commit is contained in:
Claude
2026-06-02 15:32:56 +00:00
parent dd93a74bdf
commit d43f78b161
137 changed files with 4728 additions and 4541 deletions

View File

@@ -81,7 +81,7 @@ export async function POST(req: NextRequest) {
// Trigger article publish notification if published immediately
if (status === 'published' && data) {
try {
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com';
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com';
fetch(`${siteUrl}/api/newsletter/notify-article`, {
method: 'POST',
headers: {
@@ -152,7 +152,7 @@ export async function PATCH(req: NextRequest) {
.single();
if (articleData) {
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com';
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com';
// Fire-and-forget: call notify-article route internally
fetch(`${siteUrl}/api/newsletter/notify-article`, {
method: 'POST',

View File

@@ -13,9 +13,9 @@ export async function POST(request: NextRequest) {
});
await transport.sendMail({
from: `"${process.env.SMTP_FROM_NAME || 'Broadcast Beat'}" <${process.env.SMTP_FROM_EMAIL}>`,
to: 'editor@broadcastbeat.com',
subject: `[Broadcast Beat] Blocked post attempt — banned term triggered`,
from: `"${process.env.SMTP_FROM_NAME || 'AV Beat'}" <${process.env.SMTP_FROM_EMAIL}>`,
to: 'editor@avbeat.com',
subject: `[AV Beat] Blocked post attempt — banned term triggered`,
html: `
<div style="font-family:Arial,sans-serif;max-width:600px;background:#0a0a0a;color:#e5e5e5;padding:32px;border-radius:8px;">
<h2 style="color:#ef4444;margin:0 0 16px;">Blocked Post Attempt</h2>

View File

@@ -30,7 +30,7 @@ export async function POST(request: NextRequest) {
const titleLower = (post.title || '').toLowerCase();
for (const bt of bannedTerms) {
const termLower = bt.term.toLowerCase();
const siteMatch = bt.site_scope === 'both' || (bt.site_scope === 'broadcastbeat' && post.site_id === 1) || (bt.site_scope === 'avbeat' && post.site_id === 2);
const siteMatch = bt.site_scope === 'both' || (bt.site_scope === 'avbeat' && post.site_id === 1) || (bt.site_scope === 'avbeat' && post.site_id === 2);
if (siteMatch && titleLower.includes(termLower)) {
matches.push({ title: post.title, author: post.author_name, published_at: post.published_at, site_id: post.site_id, matched_term: bt.term, ban_level: bt.ban_level });
break; // one match per post

View File

@@ -13,14 +13,14 @@ export async function POST(request: NextRequest) {
const companyList = (companies || []).slice(0, 10).map((c: string) => `<li>${c}</li>`).join('');
const moreText = companies?.length > 10 ? `<p style="color:#888;font-size:13px;">...and ${companies.length - 10} more</p>` : '';
await transport.sendMail({
from: `"${process.env.SMTP_FROM_NAME || 'Broadcast Beat'}" <${process.env.SMTP_FROM_EMAIL}>`,
from: `"${process.env.SMTP_FROM_NAME || 'AV Beat'}" <${process.env.SMTP_FROM_EMAIL}>`,
to: email || 'ryan.salazar@relevantmediaproperties.com',
subject: `[Relevant Media Properties] ${count} new company story suggestion${count !== 1 ? 's' : ''} queued`,
html: `
<div style="font-family:Arial,sans-serif;max-width:600px;background:#0a0a0a;color:#e5e5e5;padding:32px;border-radius:8px;">
<div style="margin-bottom:24px;">
<h1 style="color:#3b82f6;font-size:20px;margin:0 0 4px;">Relevant Media Properties</h1>
<p style="color:#555;font-size:12px;margin:0;">Broadcast Beat · AV Beat — Company Story Engine</p>
<p style="color:#555;font-size:12px;margin:0;">AV Beat · AV Beat — Company Story Engine</p>
</div>
<h2 style="font-size:18px;color:#fff;margin:0 0 12px;">New Company Stories Queued for Review</h2>
<p style="color:#aaa;font-size:14px;line-height:1.6;margin:0 0 20px;">
@@ -31,12 +31,12 @@ export async function POST(request: NextRequest) {
<ul style="margin:0;padding-left:20px;color:#e5e5e5;font-size:14px;line-height:1.8;">${companyList}</ul>
${moreText}
</div>
<a href="${process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com'}/admin/company-tracker"
<a href="${process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com'}/admin/company-tracker"
style="display:inline-block;background:#3b82f6;color:#fff;text-decoration:none;padding:10px 20px;border-radius:6px;font-size:14px;font-weight:bold;">
Review Story Queue →
</a>
<p style="color:#444;font-size:12px;margin-top:24px;">
Manage settings at <a href="${process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com'}/admin/company-tracker" style="color:#3b82f6;">Company Tracker</a>
Manage settings at <a href="${process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com'}/admin/company-tracker" style="color:#3b82f6;">Company Tracker</a>
</p>
</div>
`,

View File

@@ -23,7 +23,7 @@ function pickPenName(siteId: number, recentNames: string[]): string {
const available = pool.filter(n => !recentNames.slice(-3).includes(n));
return available[Math.floor(Math.random() * available.length)] || pool[0];
}
// Broadcast Beat: fixed byline (Ryan) unless overridden to "rotate".
// AV Beat: fixed byline (Ryan) unless overridden to "rotate".
if (BB_AUTOSTORY_BYLINE.toLowerCase() === 'rotate') {
const pool = BB_PEN_NAMES_LEGACY;
const available = pool.filter(n => !recentNames.slice(-3).includes(n));
@@ -224,7 +224,7 @@ export async function POST(request: NextRequest) {
if ((count || 0) >= 3) continue; // max 3 per week
const siteId = article.site_id || 1;
const siteName = siteId === 2 ? 'AV Beat' : 'Broadcast Beat';
const siteName = siteId === 2 ? 'AV Beat' : 'AV Beat';
const penName = pickPenName(siteId, recentPenNames);
recentPenNames.push(penName);

View File

@@ -432,7 +432,7 @@ export async function POST(request: NextRequest) {
const penName = pickPenNameForShow(site_id, story_type, recentNames, preferred_pen_name);
const penNameData = (site_id === 2 ? AV_PEN_NAMES : BB_PEN_NAMES).find(p => p.name === penName);
const phase = getShowPhase(event);
const siteName = site_id === 2 ? 'AV Beat' : 'Broadcast Beat';
const siteName = site_id === 2 ? 'AV Beat' : 'AV Beat';
const story = await generateShowStory({
penName,

View File

@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import nodemailer from 'nodemailer';
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com';
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com';
async function sendSuspensionEmail(
email: string,
@@ -35,8 +35,8 @@ async function sendSuspensionEmail(
: '';
const subject = isBanned
? `Your Broadcast Beat account has been banned`
: `Your Broadcast Beat account has been suspended`;
? `Your AV Beat account has been banned`
: `Your AV Beat account has been suspended`;
const html = `
<!DOCTYPE html>
@@ -45,7 +45,7 @@ async function sendSuspensionEmail(
<body style="background:#0d1117;color:#e5e7eb;font-family:system-ui,sans-serif;margin:0;padding:0;">
<div style="max-width:560px;margin:40px auto;padding:32px;background:#111827;border:1px solid #1f2937;border-radius:12px;">
<div style="margin-bottom:24px;">
<span style="font-size:24px;font-weight:800;color:#fff;">Broadcast Beat</span>
<span style="font-size:24px;font-weight:800;color:#fff;">AV Beat</span>
</div>
<div style="background:${isBanned ? '#7f1d1d' : '#78350f'};border:1px solid ${isBanned ? '#991b1b' : '#92400e'};border-radius:8px;padding:16px;margin-bottom:24px;">
<p style="margin:0;font-size:14px;font-weight:700;color:${isBanned ? '#fca5a5' : '#fcd34d'};">
@@ -54,7 +54,7 @@ async function sendSuspensionEmail(
</div>
<p style="color:#d1d5db;font-size:15px;line-height:1.6;">Hi ${fullName || 'there'},</p>
<p style="color:#d1d5db;font-size:15px;line-height:1.6;">
Your Broadcast Beat account has been <strong style="color:#fff;">${isBanned ? 'permanently banned' : `suspended ${durationText}`}</strong>.
Your AV Beat account has been <strong style="color:#fff;">${isBanned ? 'permanently banned' : `suspended ${durationText}`}</strong>.
</p>
<div style="background:#0d1117;border:1px solid #1f2937;border-radius:8px;padding:16px;margin:20px 0;">
<p style="margin:0 0 8px;color:#888;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;">Reason</p>
@@ -65,7 +65,7 @@ async function sendSuspensionEmail(
${isBanned ? 'This decision is final. If you believe this is an error, please contact our support team.' : 'During this period, you will not be able to post threads or replies. If you believe this is an error, please contact our support team.'}
</p>
<div style="margin-top:24px;padding-top:24px;border-top:1px solid #1f2937;">
<p style="margin:0;color:#555;font-size:12px;">Broadcast Beat &mdash; ${siteUrl}</p>
<p style="margin:0;color:#555;font-size:12px;">AV Beat &mdash; ${siteUrl}</p>
</div>
</div>
</body>
@@ -73,7 +73,7 @@ async function sendSuspensionEmail(
`;
await transporter.sendMail({
from: `"${process.env.SMTP_FROM_NAME || 'Broadcast Beat'}" <${process.env.SMTP_FROM_EMAIL}>`,
from: `"${process.env.SMTP_FROM_NAME || 'AV Beat'}" <${process.env.SMTP_FROM_EMAIL}>`,
to: email,
subject,
html,

View File

@@ -107,7 +107,7 @@ export async function POST(req: NextRequest) {
// Send email via Supabase Edge Function
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com';
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com';
try {
const emailRes = await fetch(`${supabaseUrl}/functions/v1/send-contributor-invite`, {
@@ -118,7 +118,7 @@ export async function POST(req: NextRequest) {
},
body: JSON.stringify({
email,
inviterName: inviterProfile?.full_name || inviterProfile?.email || 'Broadcast Beat Admin',
inviterName: inviterProfile?.full_name || inviterProfile?.email || 'AV Beat Admin',
role: role || 'contributor',
token,
message,