diff --git a/src/app/api/newsletter/subscribe/route.ts b/src/app/api/newsletter/subscribe/route.ts index 9dd4c6a..e36ea69 100644 --- a/src/app/api/newsletter/subscribe/route.ts +++ b/src/app/api/newsletter/subscribe/route.ts @@ -1,76 +1,123 @@ -import { createClient } from '@/lib/supabase/server'; +import { createAdminClient } from '@/lib/supabase/admin'; import { NextRequest, NextResponse } from 'next/server'; import nodemailer from 'nodemailer'; +export const runtime = 'nodejs'; + +// Public newsletter subscribe endpoint. Used by: +// - NewsletterModal (site-wide popup) +// - Header subscribe widget +// - Homepage NewsletterSignup section +// - Footer signup +// +// Why service-role (not the session client): +// bb.newsletter_subscribers' RLS only has an INSERT policy for anon +// ('public_subscribe_newsletter') — upsert needs both INSERT+UPDATE +// to handle re-subscribers. Service-role bypasses RLS cleanly. + export async function POST(req: NextRequest) { try { - const { email, topics } = await req.json(); + const { email, topics, source } = await req.json(); if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { - return NextResponse.json({ error: 'Valid email is required' }, { status: 400 }); + return NextResponse.json({ error: 'Please enter a valid email address.' }, { status: 400 }); } - const supabase = await createClient(); + const normalized = email.toLowerCase().trim(); + const supabase = createAdminClient(); // Upsert subscriber (re-subscribe if previously unsubscribed) const { error } = await supabase .from('newsletter_subscribers') .upsert( { - email: email.toLowerCase().trim(), - topics: topics || [], + email: normalized, + topics: topics && Array.isArray(topics) ? topics : [], status: 'active', subscribed_at: new Date().toISOString(), unsubscribed_at: null, }, - { onConflict: 'email' } + { onConflict: 'email' }, ); if (error) { - console.error('Supabase error:', error); - return NextResponse.json({ error: 'Failed to save subscription' }, { status: 500 }); + console.error('newsletter_subscribers upsert failed:', error.message, error.details); + return NextResponse.json( + { error: 'Could not save subscription. Please try again in a moment.' }, + { status: 500 }, + ); } - // Send confirmation email via SMTP - const smtpHost = process.env.SMTP_HOST; - const smtpPort = parseInt(process.env.SMTP_PORT || '587'); - const smtpUser = process.env.SMTP_USER; - const smtpPass = process.env.SMTP_PASS; - const fromEmail = process.env.SMTP_FROM_EMAIL || smtpUser; - const fromName = process.env.SMTP_FROM_NAME || 'Broadcast Beat'; + // Best-effort welcome email — never block the API response on SMTP. + // The signup is already saved to the DB, so we return 200 either way + // and let the email fire in the background. + void sendWelcome(normalized, topics).catch((e) => + console.error('newsletter welcome email failed:', e?.message || e), + ); - if (smtpHost && smtpUser && smtpPass) { - const transporter = nodemailer.createTransport({ - host: smtpHost, - port: smtpPort, - secure: smtpPort === 465, - auth: { user: smtpUser, pass: smtpPass }, - }); - - const topicsList = topics && topics.length > 0 - ? `

You'll receive updates on: ${topics.join(', ')}

` - : ''; - - await transporter.sendMail({ - from: `"${fromName}" <${fromEmail}>`, - to: email, - subject: `Welcome to Broadcast Beat — You're subscribed!`, - html: ` -
-

You're in!

-

Thanks for subscribing to Broadcast Beat — the digital platform for broadcast engineering.

- ${topicsList} -

You'll be the first to know about the latest news, product launches, and industry insights.

-
-

You're receiving this because you signed up at broadcastbeat.com. Unsubscribe

-
- `, - }); - } - - return NextResponse.json({ success: true }); - } catch (err: unknown) { - console.error('Subscribe error:', err); + return NextResponse.json({ success: true, source: source || null }); + } catch (err: any) { + console.error('Subscribe handler error:', err?.message || err); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } } + +async function sendWelcome(email: string, topics?: string[]) { + const smtpHost = process.env.SMTP_HOST; + const smtpPort = parseInt(process.env.SMTP_PORT || '587', 10); + const smtpUser = process.env.SMTP_USER; + const smtpPass = process.env.SMTP_PASS; + const fromEmail = process.env.SMTP_FROM_EMAIL || smtpUser; + const fromName = process.env.SMTP_FROM_NAME || 'Broadcast Beat'; + const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com'; + + if (!smtpHost || !smtpUser || !smtpPass || !fromEmail) { + // SMTP not configured — signal that to logs but don't blow up. + console.warn('newsletter welcome: SMTP env not set; skipping send'); + return; + } + + const transporter = nodemailer.createTransport({ + host: smtpHost, + port: smtpPort, + secure: smtpPort === 465, + auth: { user: smtpUser, pass: smtpPass }, + }); + + const topicsBlock = + topics && topics.length > 0 + ? `

You'll receive coverage on: ${topics.join(', ')}

` + : ''; + + await transporter.sendMail({ + from: `"${fromName}" <${fromEmail}>`, + to: email, + subject: 'Welcome to Broadcast Beat', + html: ` + +Welcome to Broadcast Beat + + + +
+ + + + + +
+
Broadcast Beat Newsletter
+

You're in

+

Thanks for subscribing to Broadcast Beat — the digital platform for broadcast engineering professionals.

+ ${topicsBlock} +
+

You'll get one editorial dispatch a week — industry news, product launches, NAB/IBC coverage, and deep-dives. No spam, no sales calls.

+
+ Browse The Latest News +
+

You're receiving this because you signed up at broadcastbeat.com. Unsubscribe anytime.

+
+
+`, + }); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 67c18b8..7d08ef8 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,7 +1,8 @@ import React, { Suspense } from 'react'; import type { Metadata, Viewport } from 'next'; import '../styles/tailwind.css'; -import SystemStatusBar from '@/components/SystemStatusBar'; +// SystemStatusBar removed — the "articles indexed / events tracked / [v2.0.0]" +// strip was eating vertical space without serving readers. import AskBBAI from '@/components/AskBBAI'; import GoogleAnalytics from '@/components/GoogleAnalytics'; import CookieConsent from '@/components/CookieConsent'; @@ -131,7 +132,6 @@ export default function RootLayout({ -
{children}
diff --git a/src/components/FeaturedBentoFromDb.tsx b/src/components/FeaturedBentoFromDb.tsx index 9cf636f..dbce0d3 100644 --- a/src/components/FeaturedBentoFromDb.tsx +++ b/src/components/FeaturedBentoFromDb.tsx @@ -60,13 +60,16 @@ export default async function FeaturedBento() { return (
-
-

- Featured · staff editorial +
+

+ Staff Editorial

-
- - All featured → + + Selected by our newsroom + +
+ + See all →