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:
@@ -1,3 +1,6 @@
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
export const metadata = {
|
||||
title: "About BroadcastBeat",
|
||||
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() {
|
||||
return (
|
||||
<main className="mx-auto max-w-3xl px-6 py-12 text-[#e5e7eb]">
|
||||
<h1 className="font-serif text-4xl font-bold tracking-tight mb-6">About BroadcastBeat</h1>
|
||||
<div className="space-y-5 text-[#d1d5db] text-[15px] leading-relaxed">
|
||||
<p>
|
||||
BroadcastBeat is an independent trade publication covering broadcast,
|
||||
live production, streaming, and post-production technology. We
|
||||
report on the equipment, services, and people that move the
|
||||
professional video industry forward.
|
||||
</p>
|
||||
<p>
|
||||
We cover the major industry shows — NAB, IBC, BroadcastAsia, MPTS,
|
||||
and others — track product launches and acquisitions, and publish
|
||||
interviews with the engineers, post supervisors, and executives
|
||||
building the next generation of workflows.
|
||||
</p>
|
||||
<p>
|
||||
Editorial decisions are made independently of advertising. We do
|
||||
not accept payment for editorial coverage. Vendor-supplied press
|
||||
releases routed through our distribution platform are rewritten
|
||||
by our staff or AI assistants according to our editorial style
|
||||
guide; the original submission is preserved on file.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<main className="mx-auto max-w-3xl px-6 py-12 text-[#e5e7eb]">
|
||||
<h1 className="font-serif text-4xl font-bold tracking-tight mb-6">About BroadcastBeat</h1>
|
||||
<div className="space-y-5 text-[#d1d5db] text-[15px] leading-relaxed">
|
||||
<p>
|
||||
BroadcastBeat is an independent trade publication covering broadcast,
|
||||
live production, streaming, and post-production technology. We
|
||||
report on the equipment, services, and people that move the
|
||||
professional video industry forward.
|
||||
</p>
|
||||
<p>
|
||||
We cover the major industry shows — NAB, IBC, BroadcastAsia, MPTS,
|
||||
and others — track product launches and acquisitions, and publish
|
||||
interviews with the engineers, post supervisors, and executives
|
||||
building the next generation of workflows.
|
||||
</p>
|
||||
<p>
|
||||
Editorial decisions are made independently of advertising. We do
|
||||
not accept payment for editorial coverage. Vendor-supplied press
|
||||
releases routed through our distribution platform are rewritten
|
||||
by our staff or AI assistants according to our editorial style
|
||||
guide; the original submission is preserved on file.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,40 @@
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
export const metadata = { title: "Press kit — BroadcastBeat" };
|
||||
|
||||
export default function PressKitPage() {
|
||||
return (
|
||||
<main className="mx-auto max-w-3xl px-6 py-12 text-[#e5e7eb]">
|
||||
<h1 className="font-serif text-4xl font-bold tracking-tight mb-6">Press kit</h1>
|
||||
<div className="space-y-6 text-[15px]">
|
||||
<section>
|
||||
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Logos</h2>
|
||||
<ul className="space-y-1">
|
||||
<li><a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="/assets/images/logo.png" download>BroadcastBeat logo (PNG)</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Boilerplate</h2>
|
||||
<p className="text-[#d1d5db] leading-relaxed">
|
||||
BroadcastBeat is an independent trade publication covering broadcast,
|
||||
live production, streaming, and post-production technology. Founded
|
||||
to bring engineering-rigor reporting to industry coverage, BroadcastBeat
|
||||
publishes daily on the equipment, services, and people that move
|
||||
professional video forward.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Media contact</h2>
|
||||
<p>
|
||||
<a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="mailto:ryan.salazar@relevantmediaproperties.com">ryan.salazar@relevantmediaproperties.com</a>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<main className="mx-auto max-w-3xl px-6 py-12 text-[#e5e7eb]">
|
||||
<h1 className="font-serif text-4xl font-bold tracking-tight mb-6">Press kit</h1>
|
||||
<div className="space-y-6 text-[15px]">
|
||||
<section>
|
||||
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Logos</h2>
|
||||
<ul className="space-y-1">
|
||||
<li><a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="/assets/images/logo.png" download>BroadcastBeat logo (PNG)</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Boilerplate</h2>
|
||||
<p className="text-[#d1d5db] leading-relaxed">
|
||||
BroadcastBeat is an independent trade publication covering broadcast,
|
||||
live production, streaming, and post-production technology. Founded
|
||||
to bring engineering-rigor reporting to industry coverage, BroadcastBeat
|
||||
publishes daily on the equipment, services, and people that move
|
||||
professional video forward.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Media contact</h2>
|
||||
<p>
|
||||
<a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="mailto:ryan.salazar@relevantmediaproperties.com">ryan.salazar@relevantmediaproperties.com</a>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const revalidate = 600;
|
||||
@@ -32,36 +34,40 @@ export default async function TeamPage() {
|
||||
const team = (data || []) as Persona[];
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-5xl px-6 py-12 text-[#e5e7eb]">
|
||||
<h1 className="font-serif text-4xl font-bold tracking-tight mb-2">Editorial team</h1>
|
||||
<p className="text-sm text-[#9ca3af] mb-10">
|
||||
Staff journalists by beat. We are a mix of human reporters and trained
|
||||
AI personas operating under house style.
|
||||
</p>
|
||||
<ul className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
{team.map((p) => (
|
||||
<li key={p.slug} className="rounded border border-[#252525] bg-[#0b0f17] p-5 flex gap-4">
|
||||
{p.avatar_url && (
|
||||
<img
|
||||
src={p.avatar_url}
|
||||
alt={p.name}
|
||||
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>
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<main className="mx-auto max-w-5xl px-6 py-12 text-[#e5e7eb]">
|
||||
<h1 className="font-serif text-4xl font-bold tracking-tight mb-2">Editorial team</h1>
|
||||
<p className="text-sm text-[#9ca3af] mb-10">
|
||||
Staff journalists by beat. We are a mix of human reporters and trained
|
||||
AI personas operating under house style.
|
||||
</p>
|
||||
<ul className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
{team.map((p) => (
|
||||
<li key={p.slug} className="rounded border border-[#252525] bg-[#0b0f17] p-5 flex gap-4">
|
||||
{p.avatar_url && (
|
||||
<img
|
||||
src={p.avatar_url}
|
||||
alt={p.name}
|
||||
width={64}
|
||||
height={64}
|
||||
className="rounded-full border border-[#252525]"
|
||||
/>
|
||||
)}
|
||||
{p.bio && <p className="text-sm text-[#9ca3af] mt-2 leading-relaxed">{p.bio}</p>}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</main>
|
||||
<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>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
11
src/app/newsletter/page.tsx
Normal file
11
src/app/newsletter/page.tsx
Normal 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
60
src/app/rss/route.ts
Normal 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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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 & Intelligence for Broadcast, Post-Production & 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",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -8,7 +8,6 @@ const ITEMS = [
|
||||
{ label: "Editorial team", href: "/about/team" },
|
||||
{ label: "Contact", href: "/about/contact" },
|
||||
{ label: "Advertise", href: "/about/advertise" },
|
||||
{ label: "Press kit", href: "/about/press-kit" },
|
||||
];
|
||||
|
||||
export default function AboutDropdown() {
|
||||
|
||||
@@ -6,7 +6,6 @@ import AppImage from "@/components/ui/AppImage";
|
||||
import {
|
||||
LinkedInIcon,
|
||||
InstagramIcon,
|
||||
TwitterXIcon,
|
||||
FacebookIcon,
|
||||
RssIcon,
|
||||
MenuIcon,
|
||||
@@ -215,19 +214,15 @@ export default function Header() {
|
||||
News & Intelligence for Broadcast, Post-Production & Streaming Technology
|
||||
</p>
|
||||
<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">
|
||||
<LinkedInIcon size={14} />
|
||||
</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">
|
||||
<InstagramIcon size={14} />
|
||||
</a>
|
||||
<a href="https://twitter.com" target="_blank" rel="noopener noreferrer" aria-label="BroadcastBeat on Twitter/X"
|
||||
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"
|
||||
<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">
|
||||
<FacebookIcon size={14} />
|
||||
</a>
|
||||
|
||||
@@ -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="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.sources.toLocaleString()} wire sources</span>
|
||||
<span>{stats.events} events tracked</span>
|
||||
</div>
|
||||
<span className="text-[var(--color-text-info,#60a5fa)]">[{BUILD_VERSION}]</span>
|
||||
|
||||
Reference in New Issue
Block a user