delete sampleArticles seed; serve all sections from bb.wp_imported_posts (DB)

Eliminates 72 hardcoded rocket.new image refs in one shot. Section pages
(/gear, /technology, /show-coverage, /news) now render from
bb.wp_imported_posts via getLegacyArticlesBySection(section), which maps
each non-news section to a curated tag set (e.g. gear -> Audio, Cameras,
lighting, Blackmagic Design, monitoring; technology -> AI, Streaming,
OTT, IP, Cloud, AV; show-coverage -> NAB, IBC, BroadcastAsia).

Also:
- src/lib/articles/types.ts: Article interface lifted out of the deleted
  seed file. Type-only consumers (NewsArticleDetailClient,
  ArticleDetailClient, legacy-source) re-imported from here.
- /news (client component) now wrapped: server fetches articles from DB
  and passes to NewsPageClient. Filter UI unchanged.
- generateStaticParams on /news/[slug] and /articles/[slug] no longer
  references seed slugs; pre-renders 50 most-recent imported slugs and
  relies on dynamicParams + revalidate=3600 for the rest.
- sitemap.ts now sources article URLs from
  getLegacyRecentSitemapEntries() (up to 5000 most-recent imported posts).
  Default BASE_URL fallback updated from the rocket.new placeholder to
  bb-staging.onsethost.com.
- src/app/api/ai/article-suggestions/route.ts now pulls candidates from
  the DB (top 100 recent news) and resolves AI-returned slugs via DB
  lookup if not in the candidate window.

Inline rocket.new image refs in homepage components (ArticleFeed,
FeaturedBento) are unchanged in this commit; those are inline seed
arrays in the components, not imports of sampleArticles.
This commit is contained in:
Ryan Salazar
2026-05-08 14:31:25 +00:00
parent 08df1ad4c1
commit f1e1d5ea76
14 changed files with 483 additions and 1321 deletions

View File

@@ -1,22 +1,26 @@
import { NextRequest, NextResponse } from 'next/server';
import { hybridAI } from '@/lib/ai/hybridRouter';
import { sampleArticles } from '@/lib/articles/sampleArticles';
import {
getLegacyArticlesBySection,
getLegacyArticleBySlug,
} from '@/lib/articles/legacy-source';
export async function POST(request: NextRequest) {
try {
const { interests, readingHistory } = await request.json();
// Build a compact article index for the AI to reason over
const articleIndex = sampleArticles
.filter((a) => a.section === 'news')
.map((a) => ({
slug: a.slug,
title: a.title,
excerpt: a.excerpt,
tags: a.tags,
category: a.category,
date: a.date,
}));
// Pull a recent slice of news for the AI to choose from. Cap at 100 so the
// prompt stays bounded.
const news = await getLegacyArticlesBySection('news', 100);
const articleIndex = news.map((a) => ({
slug: a.slug,
title: a.title,
excerpt: a.excerpt,
tags: a.tags,
category: a.category,
date: a.date,
}));
const systemPrompt = `You are a personalized content recommendation engine for BroadcastBeat, a broadcast engineering news platform.
Given a reader's topic interests and reading history, select the 4 most relevant articles from the provided article list.
@@ -36,45 +40,38 @@ Return the 4 most relevant article slugs as a JSON array.`;
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt },
],
{
maxTokens: 200,
temperature: 0.3,
priority: 4,
}
{ maxTokens: 200, temperature: 0.3, priority: 4 }
);
const content = result.text;
let slugs: string[] = [];
try {
// Extract JSON array from response (model may wrap it in text)
const match = content.match(/\[[\s\S]*?\]/);
slugs = match ? JSON.parse(match[0]) : JSON.parse(content.trim());
} catch {
// Fallback: extract slugs with regex
const matches = content.match(/"([^"]+)"/g);
slugs = matches ? matches.map((m: string) => m.replace(/"/g, '')).slice(0, 4) : [];
}
// Resolve full article objects
const suggested = slugs
.map((slug: string) => sampleArticles.find((a) => a.slug === slug))
.filter(Boolean)
.slice(0, 4);
// Resolve from the candidate set first; if a slug wasn't in the recent
// window, look it up via DB so AI hallucinations or older slugs still resolve.
const suggested = (
await Promise.all(
slugs.slice(0, 4).map(async (slug: string) =>
news.find((a) => a.slug === slug) || (await getLegacyArticleBySlug(slug))
)
)
).filter(Boolean);
// Fallback: return latest news if AI returned nothing useful
if (suggested.length === 0) {
const fallback = sampleArticles
.filter((a) => a.section === 'news')
.slice(0, 4);
return NextResponse.json({ suggestions: fallback });
return NextResponse.json({ suggestions: news.slice(0, 4) });
}
return NextResponse.json({ suggestions: suggested });
} catch (error) {
console.error('Article suggestions error:', error);
// Graceful fallback
const fallback = sampleArticles.filter((a) => a.section === 'news').slice(0, 4);
const fallback = await getLegacyArticlesBySection('news', 4);
return NextResponse.json({ suggestions: fallback });
}
}

View File

@@ -5,7 +5,7 @@ import AppImage from "@/components/ui/AppImage";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { createClient } from "@/lib/supabase/client";
import type { Article } from "@/lib/articles/sampleArticles";
import type { Article } from "@/lib/articles/types";
// DO NOT OVERRIDE — ArticleDetailClient interface and session tracking
interface ArticleDetailClientProps {

View File

@@ -1,7 +1,7 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import ArticleDetailPage from "./ArticleDetailClient";
import { sampleArticles, getRelatedArticles, type Article } from "@/lib/articles/sampleArticles";
import type { Article } from "@/lib/articles/types";
import {
getLegacyArticleBySlug,
getLegacyRecentSlugs,
@@ -22,10 +22,6 @@ async function loadArticle(slug: string): Promise<{
article: Article | null;
related: Article[];
}> {
const seed = sampleArticles.find((a) => a.slug === slug);
if (seed) {
return { article: seed, related: getRelatedArticles(slug, 3) };
}
const imported = await getLegacyArticleBySlug(slug);
if (!imported) return { article: null, related: [] };
const related = await getLegacyRelatedArticles(slug, imported.category, 3);
@@ -63,9 +59,8 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
}
export async function generateStaticParams() {
const seedSlugs = sampleArticles.map((a) => a.slug);
const recent = await getLegacyRecentSlugs(50);
return Array.from(new Set([...seedSlugs, ...recent])).map((slug) => ({ slug }));
return recent.map((slug) => ({ slug }));
}
export default async function ArticlePage({ params }: PageProps) {

View File

@@ -4,7 +4,9 @@ import Link from "next/link";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import AppImage from "@/components/ui/AppImage";
import { getArticlesBySection } from "@/lib/articles/sampleArticles";
import { getLegacyArticlesBySection } from "@/lib/articles/legacy-source";
export const revalidate = 1800;
export const metadata: Metadata = {
title: "Broadcast Gear & Reviews — BroadcastBeat",
@@ -18,8 +20,8 @@ export const metadata: Metadata = {
},
};
export default function GearPage() {
const articles = getArticlesBySection("gear");
export default async function GearPage() {
const articles = await getLegacyArticlesBySection("gear");
return (
<div className="min-h-screen bg-background">

View File

@@ -0,0 +1,293 @@
"use client";
import React, { useState, useMemo } from "react";
import Link from "next/link";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import AppImage from "@/components/ui/AppImage";
import type { Article } from "@/lib/articles/types";
import AISuggestedArticles from "@/components/AISuggestedArticles";
const CATEGORIES = ["All", "Cloud", "AI", "IP Workflows", "Live Production", "Streaming", "Audio", "Cameras", "NAB 2026", "EAS", "Sports Broadcasting", "Ad Tech"];
const SORT_OPTIONS = [
{ value: "newest", label: "Newest First" },
{ value: "oldest", label: "Oldest First" },
{ value: "az", label: "A → Z" },
{ value: "za", label: "Z → A" },
];
function parseArticleDate(dateStr: string): Date {
return new Date(dateStr);
}
export default function NewsPage({ articles }: { articles: Article[] }) {
const allArticles = articles;
const [search, setSearch] = useState("");
const [activeCategory, setActiveCategory] = useState("All");
const [dateFrom, setDateFrom] = useState("");
const [dateTo, setDateTo] = useState("");
const [sortBy, setSortBy] = useState("newest");
const filtered = useMemo(() => {
let result = [...allArticles];
// Search filter
if (search.trim()) {
const q = search.toLowerCase();
result = result.filter(
(a) =>
a.title.toLowerCase().includes(q) ||
a.excerpt.toLowerCase().includes(q) ||
a.author.toLowerCase().includes(q) ||
a.tags.some((t) => t.toLowerCase().includes(q))
);
}
// Category filter
if (activeCategory !== "All") {
result = result.filter((a) =>
a.tags.some((t) => t.toLowerCase() === activeCategory.toLowerCase()) ||
a.category.toLowerCase().includes(activeCategory.toLowerCase())
);
}
// Date range filter
if (dateFrom) {
const from = new Date(dateFrom);
result = result.filter((a) => parseArticleDate(a.date) >= from);
}
if (dateTo) {
const to = new Date(dateTo);
to.setHours(23, 59, 59, 999);
result = result.filter((a) => parseArticleDate(a.date) <= to);
}
// Sorting
result.sort((a, b) => {
if (sortBy === "newest") return parseArticleDate(b.date).getTime() - parseArticleDate(a.date).getTime();
if (sortBy === "oldest") return parseArticleDate(a.date).getTime() - parseArticleDate(b.date).getTime();
if (sortBy === "az") return a.title.localeCompare(b.title);
if (sortBy === "za") return b.title.localeCompare(a.title);
return 0;
});
return result;
}, [allArticles, search, activeCategory, dateFrom, dateTo, sortBy]);
const hasActiveFilters = search || activeCategory !== "All" || dateFrom || dateTo || sortBy !== "newest";
function clearFilters() {
setSearch("");
setActiveCategory("All");
setDateFrom("");
setDateTo("");
setSortBy("newest");
}
return (
<div className="min-h-screen bg-background">
<Header />
{/* DO NOT OVERRIDE — News page hero header */}
<div className="bg-[#111] border-b border-[#222] py-8">
<div className="max-w-container mx-auto px-4">
<div className="flex items-center gap-3 mb-2">
<span className="section-label">News</span>
<div className="flex-1 h-px bg-[#2a2a2a]" />
</div>
<h1 className="font-heading text-white text-3xl font-bold">Latest Broadcast News</h1>
<p className="text-[#777] font-body text-sm mt-2">Breaking news and industry updates from the world of broadcast engineering</p>
</div>
</div>
{/* Search & Filter Bar */}
<div className="bg-[#0d0d0d] border-b border-[#1e1e1e] py-4">
<div className="max-w-container mx-auto px-4 space-y-3">
{/* Row 1: Search + Sort */}
<div className="flex flex-col sm:flex-row gap-3">
{/* Search */}
<div className="relative flex-1">
<svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#555]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z" />
</svg>
<input
type="text"
placeholder="Search articles, topics, authors…"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full bg-[#1a1a1a] border border-[#2a2a2a] text-[#e0e0e0] placeholder-[#555] text-sm font-body pl-9 pr-4 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] transition-colors"
/>
{search && (
<button
onClick={() => setSearch("")}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[#555] hover:text-[#e0e0e0] transition-colors"
aria-label="Clear search"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
{/* Date Range */}
<div className="flex items-center gap-2">
<input
type="date"
value={dateFrom}
onChange={(e) => setDateFrom(e.target.value)}
className="bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] transition-colors [color-scheme:dark]"
title="From date"
/>
<span className="text-[#444] text-xs"></span>
<input
type="date"
value={dateTo}
onChange={(e) => setDateTo(e.target.value)}
className="bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] transition-colors [color-scheme:dark]"
title="To date"
/>
</div>
{/* Sort */}
<div className="relative">
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
className="appearance-none bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body pl-3 pr-8 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] transition-colors cursor-pointer"
>
{SORT_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<svg className="absolute right-2.5 top-1/2 -translate-y-1/2 w-3 h-3 text-[#555] pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
{/* Row 2: Category chips */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-[#555] text-[10px] font-body uppercase tracking-wider shrink-0">Topics:</span>
<div className="flex gap-1.5 flex-wrap">
{CATEGORIES.map((cat) => (
<button
key={cat}
onClick={() => setActiveCategory(cat)}
className={`px-2.5 py-1 text-[11px] font-body rounded-sm border transition-colors ${
activeCategory === cat
? "bg-[#3b82f6] border-[#3b82f6] text-white"
: "bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6]"
}`}
>
{cat}
</button>
))}
</div>
{hasActiveFilters && (
<button
onClick={clearFilters}
className="ml-auto text-[11px] font-body text-[#555] hover:text-[#e0e0e0] underline transition-colors shrink-0"
>
Clear all
</button>
)}
</div>
</div>
</div>
<main className="max-w-container mx-auto px-4 py-8">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Article list */}
<div className="lg:col-span-8">
{/* Results count */}
<div className="flex items-center justify-between mb-4">
<p className="text-[#555] font-body text-xs">
{filtered.length === allArticles.length
? `${allArticles.length} articles`
: `${filtered.length} of ${allArticles.length} articles`}
</p>
</div>
{filtered.length === 0 ? (
<div className="py-16 text-center">
<svg className="w-10 h-10 text-[#333] mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p className="text-[#555] font-body text-sm">No articles match your filters.</p>
<button onClick={clearFilters} className="mt-3 text-[#3b82f6] text-xs font-body hover:underline">Clear filters</button>
</div>
) : (
<div className="space-y-0">
{filtered.map((article) => (
<Link
key={article.slug}
href={`/news/${article.slug}`}
className="flex gap-4 py-5 border-b border-[#222] group hover:bg-[#111] transition-colors px-2 -mx-2">
<div className="flex-shrink-0 w-[160px] h-[105px] relative overflow-hidden rounded-sm">
<AppImage
src={article.image}
alt={article.alt}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
sizes="160px"
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1.5">
<span className="text-[#3b82f6] font-body text-[10px] font-bold uppercase tracking-wider">{article.category}</span>
</div>
<h2 className="font-heading text-[#e0e0e0] text-base font-bold leading-snug mb-2 group-hover:text-[#3b82f6] transition-colors line-clamp-2">
{article.title}
</h2>
<p className="text-[#777] font-body text-sm leading-relaxed line-clamp-2">{article.excerpt}</p>
<div className="flex items-center gap-2 mt-2">
<span className="text-[#555] font-body text-xs">By {article.author}</span>
<span className="text-[#444] text-[10px]">·</span>
<span className="text-[#555] font-body text-xs">{article.readTime}</span>
</div>
</div>
</Link>
))}
</div>
)}
</div>
{/* Sidebar */}
<aside className="lg:col-span-4 space-y-6">
{/* AI Suggested Articles */}
<AISuggestedArticles variant="full" />
<div className="bg-[#111] border border-[#222] p-4">
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-3 pb-2 border-b border-[#222]">
Popular Tags
</h3>
<div className="flex flex-wrap gap-2">
{["IP Workflows", "Cloud", "AI", "Live Production", "Streaming", "NAB 2026", "Audio", "Cameras"].map((tag) => (
<button
key={tag}
onClick={() => setActiveCategory(tag)}
className={`px-2 py-1 border text-xs font-body rounded-sm transition-colors ${
activeCategory === tag
? "bg-[#3b82f6] border-[#3b82f6] text-white"
: "bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6]"
}`}
>
{tag}
</button>
))}
</div>
</div>
<div className="bg-[#0d1520] border border-[#1e3a5f] p-4">
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-2">Newsletter</h3>
<p className="text-[#777] text-xs font-body mb-3">Get broadcast news delivered to your inbox every week.</p>
<Link href="/home-page#newsletter" className="btn-subscribe text-xs py-2 px-4 inline-block">Subscribe Free</Link>
</div>
</aside>
</div>
</main>
<Footer />
</div>
);
}

View File

@@ -6,7 +6,7 @@ import Header from "@/components/Header";
import Footer from "@/components/Footer";
import AdSlot from "@/components/AdSlot";
import { createClient } from "@/lib/supabase/client";
import type { Article } from "@/lib/articles/sampleArticles";
import type { Article } from "@/lib/articles/types";
interface NewsArticleDetailClientProps {
article: Article;

View File

@@ -1,6 +1,6 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { sampleArticles, getRelatedArticles, type Article } from "@/lib/articles/sampleArticles";
import type { Article } from "@/lib/articles/types";
import {
getLegacyArticleBySlug,
getLegacyRecentSlugs,
@@ -23,17 +23,11 @@ const SITE_URL =
async function loadArticle(slug: string): Promise<{
article: Article | null;
related: Article[];
source: "seed" | "imported";
}> {
// Seed (sampleArticles) wins for routes already curated.
const seed = sampleArticles.find((a) => a.slug === slug && a.section === "news");
if (seed) {
return { article: seed, related: getRelatedArticles(slug, 6), source: "seed" };
}
const imported = await getLegacyArticleBySlug(slug);
if (!imported) return { article: null, related: [], source: "imported" };
if (!imported) return { article: null, related: [] };
const related = await getLegacyRelatedArticles(slug, imported.category, 6);
return { article: imported, related, source: "imported" };
return { article: imported, related };
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
@@ -67,13 +61,10 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
}
export async function generateStaticParams() {
// Build-time: only seed articles + the most-recent imported slugs (cap to keep
// build fast). Anything else is on-demand ISR via dynamicParams=true.
const seedSlugs = sampleArticles
.filter((a) => a.section === "news")
.map((a) => a.slug);
// Build-time: pre-render the most-recent imported slugs (cap to keep the
// build fast). Everything else is on-demand ISR via dynamicParams=true.
const recent = await getLegacyRecentSlugs(50);
return Array.from(new Set([...seedSlugs, ...recent])).map((slug) => ({ slug }));
return recent.map((slug) => ({ slug }));
}
export default async function NewsArticlePage({ params }: PageProps) {

View File

@@ -1,293 +1,17 @@
"use client";
import type { Metadata } from "next";
import NewsPageClient from "./NewsPageClient";
import { getLegacyArticlesBySection } from "@/lib/articles/legacy-source";
import React, { useState, useMemo } from "react";
import Link from "next/link";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import AppImage from "@/components/ui/AppImage";
import { getArticlesBySection } from "@/lib/articles/sampleArticles";
import AISuggestedArticles from "@/components/AISuggestedArticles";
export const revalidate = 1800;
const CATEGORIES = ["All", "Cloud", "AI", "IP Workflows", "Live Production", "Streaming", "Audio", "Cameras", "NAB 2026", "EAS", "Sports Broadcasting", "Ad Tech"];
export const metadata: Metadata = {
title: "Latest Broadcast News — BroadcastBeat",
description:
"Breaking news and industry updates from the world of broadcast engineering, IP/cloud workflows, streaming, AI, and live production.",
alternates: { canonical: "/news" },
};
const SORT_OPTIONS = [
{ value: "newest", label: "Newest First" },
{ value: "oldest", label: "Oldest First" },
{ value: "az", label: "A → Z" },
{ value: "za", label: "Z → A" },
];
function parseArticleDate(dateStr: string): Date {
return new Date(dateStr);
}
export default function NewsPage() {
const allArticles = getArticlesBySection("news");
const [search, setSearch] = useState("");
const [activeCategory, setActiveCategory] = useState("All");
const [dateFrom, setDateFrom] = useState("");
const [dateTo, setDateTo] = useState("");
const [sortBy, setSortBy] = useState("newest");
const filtered = useMemo(() => {
let result = [...allArticles];
// Search filter
if (search.trim()) {
const q = search.toLowerCase();
result = result.filter(
(a) =>
a.title.toLowerCase().includes(q) ||
a.excerpt.toLowerCase().includes(q) ||
a.author.toLowerCase().includes(q) ||
a.tags.some((t) => t.toLowerCase().includes(q))
);
}
// Category filter
if (activeCategory !== "All") {
result = result.filter((a) =>
a.tags.some((t) => t.toLowerCase() === activeCategory.toLowerCase()) ||
a.category.toLowerCase().includes(activeCategory.toLowerCase())
);
}
// Date range filter
if (dateFrom) {
const from = new Date(dateFrom);
result = result.filter((a) => parseArticleDate(a.date) >= from);
}
if (dateTo) {
const to = new Date(dateTo);
to.setHours(23, 59, 59, 999);
result = result.filter((a) => parseArticleDate(a.date) <= to);
}
// Sorting
result.sort((a, b) => {
if (sortBy === "newest") return parseArticleDate(b.date).getTime() - parseArticleDate(a.date).getTime();
if (sortBy === "oldest") return parseArticleDate(a.date).getTime() - parseArticleDate(b.date).getTime();
if (sortBy === "az") return a.title.localeCompare(b.title);
if (sortBy === "za") return b.title.localeCompare(a.title);
return 0;
});
return result;
}, [allArticles, search, activeCategory, dateFrom, dateTo, sortBy]);
const hasActiveFilters = search || activeCategory !== "All" || dateFrom || dateTo || sortBy !== "newest";
function clearFilters() {
setSearch("");
setActiveCategory("All");
setDateFrom("");
setDateTo("");
setSortBy("newest");
}
return (
<div className="min-h-screen bg-background">
<Header />
{/* DO NOT OVERRIDE — News page hero header */}
<div className="bg-[#111] border-b border-[#222] py-8">
<div className="max-w-container mx-auto px-4">
<div className="flex items-center gap-3 mb-2">
<span className="section-label">News</span>
<div className="flex-1 h-px bg-[#2a2a2a]" />
</div>
<h1 className="font-heading text-white text-3xl font-bold">Latest Broadcast News</h1>
<p className="text-[#777] font-body text-sm mt-2">Breaking news and industry updates from the world of broadcast engineering</p>
</div>
</div>
{/* Search & Filter Bar */}
<div className="bg-[#0d0d0d] border-b border-[#1e1e1e] py-4">
<div className="max-w-container mx-auto px-4 space-y-3">
{/* Row 1: Search + Sort */}
<div className="flex flex-col sm:flex-row gap-3">
{/* Search */}
<div className="relative flex-1">
<svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#555]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z" />
</svg>
<input
type="text"
placeholder="Search articles, topics, authors…"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full bg-[#1a1a1a] border border-[#2a2a2a] text-[#e0e0e0] placeholder-[#555] text-sm font-body pl-9 pr-4 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] transition-colors"
/>
{search && (
<button
onClick={() => setSearch("")}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[#555] hover:text-[#e0e0e0] transition-colors"
aria-label="Clear search"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
{/* Date Range */}
<div className="flex items-center gap-2">
<input
type="date"
value={dateFrom}
onChange={(e) => setDateFrom(e.target.value)}
className="bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] transition-colors [color-scheme:dark]"
title="From date"
/>
<span className="text-[#444] text-xs"></span>
<input
type="date"
value={dateTo}
onChange={(e) => setDateTo(e.target.value)}
className="bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] transition-colors [color-scheme:dark]"
title="To date"
/>
</div>
{/* Sort */}
<div className="relative">
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
className="appearance-none bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body pl-3 pr-8 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] transition-colors cursor-pointer"
>
{SORT_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<svg className="absolute right-2.5 top-1/2 -translate-y-1/2 w-3 h-3 text-[#555] pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
{/* Row 2: Category chips */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-[#555] text-[10px] font-body uppercase tracking-wider shrink-0">Topics:</span>
<div className="flex gap-1.5 flex-wrap">
{CATEGORIES.map((cat) => (
<button
key={cat}
onClick={() => setActiveCategory(cat)}
className={`px-2.5 py-1 text-[11px] font-body rounded-sm border transition-colors ${
activeCategory === cat
? "bg-[#3b82f6] border-[#3b82f6] text-white"
: "bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6]"
}`}
>
{cat}
</button>
))}
</div>
{hasActiveFilters && (
<button
onClick={clearFilters}
className="ml-auto text-[11px] font-body text-[#555] hover:text-[#e0e0e0] underline transition-colors shrink-0"
>
Clear all
</button>
)}
</div>
</div>
</div>
<main className="max-w-container mx-auto px-4 py-8">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Article list */}
<div className="lg:col-span-8">
{/* Results count */}
<div className="flex items-center justify-between mb-4">
<p className="text-[#555] font-body text-xs">
{filtered.length === allArticles.length
? `${allArticles.length} articles`
: `${filtered.length} of ${allArticles.length} articles`}
</p>
</div>
{filtered.length === 0 ? (
<div className="py-16 text-center">
<svg className="w-10 h-10 text-[#333] mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p className="text-[#555] font-body text-sm">No articles match your filters.</p>
<button onClick={clearFilters} className="mt-3 text-[#3b82f6] text-xs font-body hover:underline">Clear filters</button>
</div>
) : (
<div className="space-y-0">
{filtered.map((article) => (
<Link
key={article.slug}
href={`/news/${article.slug}`}
className="flex gap-4 py-5 border-b border-[#222] group hover:bg-[#111] transition-colors px-2 -mx-2">
<div className="flex-shrink-0 w-[160px] h-[105px] relative overflow-hidden rounded-sm">
<AppImage
src={article.image}
alt={article.alt}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
sizes="160px"
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1.5">
<span className="text-[#3b82f6] font-body text-[10px] font-bold uppercase tracking-wider">{article.category}</span>
</div>
<h2 className="font-heading text-[#e0e0e0] text-base font-bold leading-snug mb-2 group-hover:text-[#3b82f6] transition-colors line-clamp-2">
{article.title}
</h2>
<p className="text-[#777] font-body text-sm leading-relaxed line-clamp-2">{article.excerpt}</p>
<div className="flex items-center gap-2 mt-2">
<span className="text-[#555] font-body text-xs">By {article.author}</span>
<span className="text-[#444] text-[10px]">·</span>
<span className="text-[#555] font-body text-xs">{article.readTime}</span>
</div>
</div>
</Link>
))}
</div>
)}
</div>
{/* Sidebar */}
<aside className="lg:col-span-4 space-y-6">
{/* AI Suggested Articles */}
<AISuggestedArticles variant="full" />
<div className="bg-[#111] border border-[#222] p-4">
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-3 pb-2 border-b border-[#222]">
Popular Tags
</h3>
<div className="flex flex-wrap gap-2">
{["IP Workflows", "Cloud", "AI", "Live Production", "Streaming", "NAB 2026", "Audio", "Cameras"].map((tag) => (
<button
key={tag}
onClick={() => setActiveCategory(tag)}
className={`px-2 py-1 border text-xs font-body rounded-sm transition-colors ${
activeCategory === tag
? "bg-[#3b82f6] border-[#3b82f6] text-white"
: "bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6]"
}`}
>
{tag}
</button>
))}
</div>
</div>
<div className="bg-[#0d1520] border border-[#1e3a5f] p-4">
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-2">Newsletter</h3>
<p className="text-[#777] text-xs font-body mb-3">Get broadcast news delivered to your inbox every week.</p>
<Link href="/home-page#newsletter" className="btn-subscribe text-xs py-2 px-4 inline-block">Subscribe Free</Link>
</div>
</aside>
</div>
</main>
<Footer />
</div>
);
export default async function NewsPage() {
const articles = await getLegacyArticlesBySection("news", 200);
return <NewsPageClient articles={articles} />;
}

View File

@@ -4,7 +4,9 @@ import Link from "next/link";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import AppImage from "@/components/ui/AppImage";
import { getArticlesBySection } from "@/lib/articles/sampleArticles";
import { getLegacyArticlesBySection } from "@/lib/articles/legacy-source";
export const revalidate = 1800;
export const metadata: Metadata = {
title: "NAB Show, IBC & Broadcast Event Coverage — BroadcastBeat",
@@ -20,8 +22,8 @@ export const metadata: Metadata = {
},
};
export default function ShowCoveragePage() {
const articles = getArticlesBySection("show-coverage");
export default async function ShowCoveragePage() {
const articles = await getLegacyArticlesBySection("show-coverage");
const nabArticles = articles.filter((a) => a.tags.includes("NAB 2026"));
return (

View File

@@ -1,7 +1,7 @@
import { MetadataRoute } from 'next';
import { sampleArticles } from '@/lib/articles/sampleArticles';
import { getLegacyRecentSitemapEntries } from '@/lib/articles/legacy-source';
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastb5322.builtwithrocket.new';
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://bb-staging.onsethost.com';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// Static pages
@@ -23,13 +23,26 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
{ url: `${BASE_URL}/terms`, lastModified: new Date(), changeFrequency: 'yearly', priority: 0.3 },
];
// Dynamic article pages — all published articles
const articlePages: MetadataRoute.Sitemap = sampleArticles.map((article) => ({
url: `${BASE_URL}/articles/${article.slug}`,
lastModified: new Date(article.date) || new Date(),
changeFrequency: 'monthly' as const,
priority: 0.7,
}));
// Dynamic article pages — sourced from bb.wp_imported_posts (DB).
const articleEntries = await getLegacyRecentSitemapEntries(5000);
const articlePages: MetadataRoute.Sitemap = articleEntries.flatMap(({ slug, date }) => {
const lastModified = new Date(date);
const safe = isNaN(lastModified.getTime()) ? new Date() : lastModified;
return [
{
url: `${BASE_URL}/news/${slug}`,
lastModified: safe,
changeFrequency: 'monthly' as const,
priority: 0.7,
},
{
url: `${BASE_URL}/articles/${slug}`,
lastModified: safe,
changeFrequency: 'monthly' as const,
priority: 0.6,
},
];
});
// Attempt to ping Google Search Console (fire-and-forget, non-blocking)
const sitemapUrl = encodeURIComponent(`${BASE_URL}/sitemap.xml`);

View File

@@ -4,7 +4,9 @@ import Link from "next/link";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import AppImage from "@/components/ui/AppImage";
import { getArticlesBySection } from "@/lib/articles/sampleArticles";
import { getLegacyArticlesBySection } from "@/lib/articles/legacy-source";
export const revalidate = 1800;
export const metadata: Metadata = {
title: "Broadcast Technology Deep Dives — BroadcastBeat",
@@ -20,8 +22,8 @@ export const metadata: Metadata = {
},
};
export default function TechnologyPage() {
const articles = getArticlesBySection("technology");
export default async function TechnologyPage() {
const articles = await getLegacyArticlesBySection("technology");
return (
<div className="min-h-screen bg-background">