slice 1+2: header social URLs, drop TwitterX/Press Kit, wrap About/Team/PressKit, add /newsletter + /rss

- Header.tsx: bare linkedin/instagram/facebook URLs → /broadcastbeat handles; remove TwitterXIcon from social bar + import (Footer retains TwitterXIcon per "DO NOT OVERRIDE" marker)
- AboutDropdown.tsx: drop "Press kit" item (the page itself stays at /about/press-kit, just delisted from menu)
- SystemStatusBar.tsx: remove "Index online" pulse-dot and "N wire sources" span (keep articles count + events count + build version)
- /about, /about/team, /about/press-kit: wrap in Header+Footer (previously rendered bare <main>)
- /newsletter/page.tsx (new): redirect to /newsletter/archive — Header was 404'ing on /newsletter
- /rss/route.ts (new): dynamic RSS feed reading from legacy-source news section; replaces 404 at /rss

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 21:49:34 +00:00
parent 09a24be3de
commit a240133ec7
8 changed files with 175 additions and 95 deletions

View File

@@ -1,3 +1,6 @@
import Header from "@/components/Header";
import Footer from "@/components/Footer";
export const metadata = { export const metadata = {
title: "About BroadcastBeat", title: "About BroadcastBeat",
description: "BroadcastBeat is an independent industry publication covering broadcast, production, and post-production technology.", description: "BroadcastBeat is an independent industry publication covering broadcast, production, and post-production technology.",
@@ -5,29 +8,33 @@ export const metadata = {
export default function AboutPage() { export default function AboutPage() {
return ( return (
<main className="mx-auto max-w-3xl px-6 py-12 text-[#e5e7eb]"> <div className="min-h-screen bg-background">
<h1 className="font-serif text-4xl font-bold tracking-tight mb-6">About BroadcastBeat</h1> <Header />
<div className="space-y-5 text-[#d1d5db] text-[15px] leading-relaxed"> <main className="mx-auto max-w-3xl px-6 py-12 text-[#e5e7eb]">
<p> <h1 className="font-serif text-4xl font-bold tracking-tight mb-6">About BroadcastBeat</h1>
BroadcastBeat is an independent trade publication covering broadcast, <div className="space-y-5 text-[#d1d5db] text-[15px] leading-relaxed">
live production, streaming, and post-production technology. We <p>
report on the equipment, services, and people that move the BroadcastBeat is an independent trade publication covering broadcast,
professional video industry forward. live production, streaming, and post-production technology. We
</p> report on the equipment, services, and people that move the
<p> professional video industry forward.
We cover the major industry shows NAB, IBC, BroadcastAsia, MPTS, </p>
and others track product launches and acquisitions, and publish <p>
interviews with the engineers, post supervisors, and executives We cover the major industry shows NAB, IBC, BroadcastAsia, MPTS,
building the next generation of workflows. and others track product launches and acquisitions, and publish
</p> interviews with the engineers, post supervisors, and executives
<p> building the next generation of workflows.
Editorial decisions are made independently of advertising. We do </p>
not accept payment for editorial coverage. Vendor-supplied press <p>
releases routed through our distribution platform are rewritten Editorial decisions are made independently of advertising. We do
by our staff or AI assistants according to our editorial style not accept payment for editorial coverage. Vendor-supplied press
guide; the original submission is preserved on file. releases routed through our distribution platform are rewritten
</p> by our staff or AI assistants according to our editorial style
</div> guide; the original submission is preserved on file.
</main> </p>
</div>
</main>
<Footer />
</div>
); );
} }

View File

@@ -1,33 +1,40 @@
import Header from "@/components/Header";
import Footer from "@/components/Footer";
export const metadata = { title: "Press kit — BroadcastBeat" }; export const metadata = { title: "Press kit — BroadcastBeat" };
export default function PressKitPage() { export default function PressKitPage() {
return ( return (
<main className="mx-auto max-w-3xl px-6 py-12 text-[#e5e7eb]"> <div className="min-h-screen bg-background">
<h1 className="font-serif text-4xl font-bold tracking-tight mb-6">Press kit</h1> <Header />
<div className="space-y-6 text-[15px]"> <main className="mx-auto max-w-3xl px-6 py-12 text-[#e5e7eb]">
<section> <h1 className="font-serif text-4xl font-bold tracking-tight mb-6">Press kit</h1>
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Logos</h2> <div className="space-y-6 text-[15px]">
<ul className="space-y-1"> <section>
<li><a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="/assets/images/logo.png" download>BroadcastBeat logo (PNG)</a></li> <h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Logos</h2>
</ul> <ul className="space-y-1">
</section> <li><a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="/assets/images/logo.png" download>BroadcastBeat logo (PNG)</a></li>
<section> </ul>
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Boilerplate</h2> </section>
<p className="text-[#d1d5db] leading-relaxed"> <section>
BroadcastBeat is an independent trade publication covering broadcast, <h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Boilerplate</h2>
live production, streaming, and post-production technology. Founded <p className="text-[#d1d5db] leading-relaxed">
to bring engineering-rigor reporting to industry coverage, BroadcastBeat BroadcastBeat is an independent trade publication covering broadcast,
publishes daily on the equipment, services, and people that move live production, streaming, and post-production technology. Founded
professional video forward. to bring engineering-rigor reporting to industry coverage, BroadcastBeat
</p> publishes daily on the equipment, services, and people that move
</section> professional video forward.
<section> </p>
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Media contact</h2> </section>
<p> <section>
<a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="mailto:ryan.salazar@relevantmediaproperties.com">ryan.salazar@relevantmediaproperties.com</a> <h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Media contact</h2>
</p> <p>
</section> <a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="mailto:ryan.salazar@relevantmediaproperties.com">ryan.salazar@relevantmediaproperties.com</a>
</div> </p>
</main> </section>
</div>
</main>
<Footer />
</div>
); );
} }

View File

@@ -1,4 +1,6 @@
import { createClient } from "@supabase/supabase-js"; import { createClient } from "@supabase/supabase-js";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export const revalidate = 600; export const revalidate = 600;
@@ -32,36 +34,40 @@ export default async function TeamPage() {
const team = (data || []) as Persona[]; const team = (data || []) as Persona[];
return ( return (
<main className="mx-auto max-w-5xl px-6 py-12 text-[#e5e7eb]"> <div className="min-h-screen bg-background">
<h1 className="font-serif text-4xl font-bold tracking-tight mb-2">Editorial team</h1> <Header />
<p className="text-sm text-[#9ca3af] mb-10"> <main className="mx-auto max-w-5xl px-6 py-12 text-[#e5e7eb]">
Staff journalists by beat. We are a mix of human reporters and trained <h1 className="font-serif text-4xl font-bold tracking-tight mb-2">Editorial team</h1>
AI personas operating under house style. <p className="text-sm text-[#9ca3af] mb-10">
</p> Staff journalists by beat. We are a mix of human reporters and trained
<ul className="grid grid-cols-1 md:grid-cols-2 gap-5"> AI personas operating under house style.
{team.map((p) => ( </p>
<li key={p.slug} className="rounded border border-[#252525] bg-[#0b0f17] p-5 flex gap-4"> <ul className="grid grid-cols-1 md:grid-cols-2 gap-5">
{p.avatar_url && ( {team.map((p) => (
<img <li key={p.slug} className="rounded border border-[#252525] bg-[#0b0f17] p-5 flex gap-4">
src={p.avatar_url} {p.avatar_url && (
alt={p.name} <img
width={64} src={p.avatar_url}
height={64} alt={p.name}
className="rounded-full border border-[#252525]" width={64}
/> height={64}
)} className="rounded-full border border-[#252525]"
<div className="flex-1"> />
<h2 className="font-serif text-lg font-semibold">{p.name}</h2>
{p.beat && (
<div className="text-[10px] font-mono uppercase tracking-wider text-[var(--color-text-info,#60a5fa)] mt-0.5">
{p.beat.replace(/-/g, " ")}
</div>
)} )}
{p.bio && <p className="text-sm text-[#9ca3af] mt-2 leading-relaxed">{p.bio}</p>} <div className="flex-1">
</div> <h2 className="font-serif text-lg font-semibold">{p.name}</h2>
</li> {p.beat && (
))} <div className="text-[10px] font-mono uppercase tracking-wider text-[var(--color-text-info,#60a5fa)] mt-0.5">
</ul> {p.beat.replace(/-/g, " ")}
</main> </div>
)}
{p.bio && <p className="text-sm text-[#9ca3af] mt-2 leading-relaxed">{p.bio}</p>}
</div>
</li>
))}
</ul>
</main>
<Footer />
</div>
); );
} }

View File

@@ -0,0 +1,11 @@
import { redirect } from "next/navigation";
export const metadata = {
title: "Newsletter — BroadcastBeat",
description: "BroadcastBeat newsletter archive.",
alternates: { canonical: "/newsletter/archive" },
};
export default function NewsletterIndex() {
redirect("/newsletter/archive");
}

60
src/app/rss/route.ts Normal file
View File

@@ -0,0 +1,60 @@
import { getLegacyArticlesBySection } from "@/lib/articles/legacy-source";
export const dynamic = "force-dynamic";
export const revalidate = 1800;
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://broadcastbeat.com";
function escapeXml(s: string): string {
return s
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}
export async function GET() {
let items = "";
try {
const articles = await getLegacyArticlesBySection("news", 50);
items = articles
.map((a) => {
const url = `${SITE_URL}/news/${a.slug}`;
const pubDate = a.date ? new Date(a.date).toUTCString() : new Date().toUTCString();
const title = escapeXml(a.title || "Untitled");
const description = escapeXml(a.excerpt || "");
return ` <item>
<title>${title}</title>
<link>${url}</link>
<guid isPermaLink="true">${url}</guid>
<pubDate>${pubDate}</pubDate>
<description>${description}</description>
</item>`;
})
.join("\n");
} catch {
// serve an empty but valid feed on failure
items = "";
}
const body = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>BroadcastBeat</title>
<link>${SITE_URL}</link>
<atom:link href="${SITE_URL}/rss" rel="self" type="application/rss+xml" />
<description>News &amp; Intelligence for Broadcast, Post-Production &amp; Streaming Technology</description>
<language>en-us</language>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
${items}
</channel>
</rss>`;
return new Response(body, {
headers: {
"Content-Type": "application/rss+xml; charset=utf-8",
"Cache-Control": "public, max-age=1800, s-maxage=1800, stale-while-revalidate=3600",
},
});
}

View File

@@ -8,7 +8,6 @@ const ITEMS = [
{ label: "Editorial team", href: "/about/team" }, { label: "Editorial team", href: "/about/team" },
{ label: "Contact", href: "/about/contact" }, { label: "Contact", href: "/about/contact" },
{ label: "Advertise", href: "/about/advertise" }, { label: "Advertise", href: "/about/advertise" },
{ label: "Press kit", href: "/about/press-kit" },
]; ];
export default function AboutDropdown() { export default function AboutDropdown() {

View File

@@ -6,7 +6,6 @@ import AppImage from "@/components/ui/AppImage";
import { import {
LinkedInIcon, LinkedInIcon,
InstagramIcon, InstagramIcon,
TwitterXIcon,
FacebookIcon, FacebookIcon,
RssIcon, RssIcon,
MenuIcon, MenuIcon,
@@ -215,19 +214,15 @@ export default function Header() {
News & Intelligence for Broadcast, Post-Production & Streaming Technology News & Intelligence for Broadcast, Post-Production & Streaming Technology
</p> </p>
<div className="flex items-center gap-3 ml-auto"> <div className="flex items-center gap-3 ml-auto">
<a href="https://linkedin.com" target="_blank" rel="noopener noreferrer" aria-label="BroadcastBeat on LinkedIn" <a href="https://linkedin.com/company/broadcastbeat" target="_blank" rel="noopener noreferrer" aria-label="BroadcastBeat on LinkedIn"
className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors social-icon"> className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors social-icon">
<LinkedInIcon size={14} /> <LinkedInIcon size={14} />
</a> </a>
<a href="https://instagram.com" target="_blank" rel="noopener noreferrer" aria-label="BroadcastBeat on Instagram" <a href="https://instagram.com/broadcastbeat" target="_blank" rel="noopener noreferrer" aria-label="BroadcastBeat on Instagram"
className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors social-icon"> className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors social-icon">
<InstagramIcon size={14} /> <InstagramIcon size={14} />
</a> </a>
<a href="https://twitter.com" target="_blank" rel="noopener noreferrer" aria-label="BroadcastBeat on Twitter/X" <a href="https://facebook.com/broadcastbeat" target="_blank" rel="noopener noreferrer" aria-label="BroadcastBeat on Facebook"
className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors social-icon">
<TwitterXIcon size={14} />
</a>
<a href="https://facebook.com" target="_blank" rel="noopener noreferrer" aria-label="BroadcastBeat on Facebook"
className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors social-icon"> className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors social-icon">
<FacebookIcon size={14} /> <FacebookIcon size={14} />
</a> </a>

View File

@@ -50,12 +50,7 @@ export default async function SystemStatusBar() {
> >
<div className="max-w-container mx-auto px-4 h-[28px] flex items-center justify-between text-[10px] font-mono uppercase tracking-wider text-[var(--color-text-tertiary,#6b7280)]"> <div className="max-w-container mx-auto px-4 h-[28px] flex items-center justify-between text-[10px] font-mono uppercase tracking-wider text-[var(--color-text-tertiary,#6b7280)]">
<div className="flex items-center gap-5"> <div className="flex items-center gap-5">
<span className="inline-flex items-center gap-1.5">
<span className="bb-pulse-dot bb-pulse-dot--green" />
Index online
</span>
<span>{stats.articles.toLocaleString()} articles indexed</span> <span>{stats.articles.toLocaleString()} articles indexed</span>
<span>{stats.sources.toLocaleString()} wire sources</span>
<span>{stats.events} events tracked</span> <span>{stats.events} events tracked</span>
</div> </div>
<span className="text-[var(--color-text-info,#60a5fa)]">[{BUILD_VERSION}]</span> <span className="text-[var(--color-text-info,#60a5fa)]">[{BUILD_VERSION}]</span>