- Standardize "NAB Show" / "2026 NAB Show" across badges, filter chips, and headings; never bare "NAB" in user-facing copy. - /manufacturers/[slug]: normalize show name at render, strip randomstring suffix on booth, hide booth unless the show is within 90 days, filter single-letter junk out of category display. - News filter "NAB Show" chip loose-matches any nab-tagged article so legacy "NAB 2026" tags still surface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
235 lines
9.9 KiB
TypeScript
235 lines
9.9 KiB
TypeScript
import type { Metadata } from "next";
|
||
import Link from "next/link";
|
||
import Header from "@/components/Header";
|
||
import Footer from "@/components/Footer";
|
||
import { createClient } from "@/lib/supabase/server";
|
||
import { createAdminClient } from "@/lib/supabase/admin";
|
||
import CompanyMentionsHover from "@/components/CompanyMentionsHover";
|
||
|
||
export const revalidate = 600;
|
||
|
||
export const metadata: Metadata = {
|
||
title: "2026 NAB Show — Live Coverage Hub | Broadcast Beat",
|
||
description:
|
||
"Broadcast Beat's live coverage of 2026 NAB Show: 1,100+ exhibitors, the latest product launches, booth-by-booth news, and editorial pick stories from the show floor.",
|
||
alternates: { canonical: "/nab-2026" },
|
||
openGraph: {
|
||
title: "2026 NAB Show — Live Coverage Hub",
|
||
description:
|
||
"Live coverage, exhibitor list, and breaking product news from 2026 NAB Show.",
|
||
type: "website",
|
||
url: "https://broadcastbeat.com/nab-2026",
|
||
},
|
||
};
|
||
|
||
type ExhibitorRow = {
|
||
slug: string;
|
||
company_name: string;
|
||
logo_url: string | null;
|
||
featured: boolean;
|
||
mention_count: number;
|
||
categories: string[] | null;
|
||
};
|
||
|
||
type ArticleRow = {
|
||
wp_slug: string;
|
||
title: string;
|
||
excerpt: string | null;
|
||
featured_image: string | null;
|
||
wp_published_at: string | null;
|
||
};
|
||
|
||
export default async function NabHub() {
|
||
const supabase = await createClient();
|
||
|
||
const { data: rawExhibitors } = await supabase
|
||
.from("tracked_companies")
|
||
.select("slug, company_name, logo_url, featured, mention_count, categories")
|
||
.eq("directory_visible", true)
|
||
.eq("exhibits_nab", true)
|
||
.not("slug", "is", null)
|
||
.order("featured", { ascending: false })
|
||
.order("mention_count", { ascending: false })
|
||
.order("company_name", { ascending: true })
|
||
.limit(2000);
|
||
|
||
const exhibitors = (rawExhibitors || []) as ExhibitorRow[];
|
||
|
||
// Sponsor lookup so the hub can bubble paying advertisers to the top.
|
||
let sponsors = new Set<string>();
|
||
try {
|
||
const adv = createAdminClient("adv");
|
||
const { data: ads } = await adv
|
||
.from("active_advertisers_by_property")
|
||
.select("company_name_lower")
|
||
.eq("property", "broadcastbeat");
|
||
sponsors = new Set((ads || []).map((r: any) => String(r.company_name_lower)));
|
||
} catch { /* tolerate */ }
|
||
|
||
// Group exhibitors: sponsors first, then everything else alphabetically.
|
||
const sponsorRows = exhibitors.filter((e) => sponsors.has(e.company_name.toLowerCase()));
|
||
const otherRows = exhibitors
|
||
.filter((e) => !sponsors.has(e.company_name.toLowerCase()))
|
||
.sort((a, b) => a.company_name.localeCompare(b.company_name));
|
||
|
||
// Recent NAB stories — anything published in the last 60 days with NAB in
|
||
// the title or tags. Cap at 30.
|
||
const sixtyDaysAgo = new Date(Date.now() - 60 * 24 * 3600 * 1000).toISOString();
|
||
const { data: rawArticles } = await supabase
|
||
.from("wp_imported_posts")
|
||
.select("wp_slug, title, excerpt, featured_image, wp_published_at")
|
||
.eq("status", "published")
|
||
.or("title.ilike.%NAB%,title.ilike.%NAB 2026%")
|
||
.gte("wp_published_at", sixtyDaysAgo)
|
||
.order("wp_published_at", { ascending: false })
|
||
.limit(30);
|
||
|
||
const articles = (rawArticles || []) as ArticleRow[];
|
||
|
||
return (
|
||
<div className="min-h-screen bg-[#0d0d0d]">
|
||
<Header />
|
||
<CompanyMentionsHover />
|
||
|
||
{/* Hero */}
|
||
<section className="bg-gradient-to-br from-amber-500/10 to-[#0d0d0d] border-b border-[#252525]">
|
||
<div className="max-w-6xl mx-auto px-4 py-10">
|
||
<div className="flex items-baseline gap-3 mb-2">
|
||
<span className="text-[10px] font-bold uppercase tracking-widest bg-amber-500/20 text-amber-300 px-2 py-0.5 rounded">
|
||
Live coverage hub
|
||
</span>
|
||
<span className="text-xs text-[#888]">April 11–16, 2026 · Las Vegas</span>
|
||
</div>
|
||
<h1 className="text-3xl md:text-5xl font-display font-bold text-[#e0e0e0] mb-3">
|
||
2026 NAB Show
|
||
</h1>
|
||
<p className="text-[#aaa] text-base max-w-3xl">
|
||
{exhibitors.length.toLocaleString()} exhibitors confirmed.
|
||
{sponsorRows.length > 0 && <> {sponsorRows.length} are Broadcast Beat coverage partners.</>}
|
||
{" "}Browse the directory, read the latest product news, and find every booth.
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
<div className="max-w-6xl mx-auto px-4 py-10 space-y-12">
|
||
|
||
{/* Featured / Coverage Partners */}
|
||
{sponsorRows.length > 0 && (
|
||
<section>
|
||
<header className="flex items-baseline justify-between mb-4">
|
||
<h2 className="text-xl font-display font-bold text-[#e0e0e0]">
|
||
Coverage Partners at the 2026 NAB Show
|
||
</h2>
|
||
<span className="text-xs text-[#888]">{sponsorRows.length} exhibiting partner{sponsorRows.length === 1 ? "" : "s"}</span>
|
||
</header>
|
||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
||
{sponsorRows.map((e) => (
|
||
<Link
|
||
key={e.slug}
|
||
href={`/manufacturers/${e.slug}`}
|
||
data-company-slug={e.slug}
|
||
className="bg-[#0d0d0d] border-2 border-emerald-500/40 rounded-lg p-3 hover:border-emerald-500/70 hover:shadow-md transition-all flex flex-col items-center text-center gap-2"
|
||
>
|
||
<div className="w-full h-16 flex items-center justify-center bg-[#1a1a1a] rounded">
|
||
{e.logo_url ? (
|
||
/* eslint-disable-next-line @next/next/no-img-element */
|
||
<img src={e.logo_url} alt={`${e.company_name} logo`} className="max-h-12 max-w-full object-contain" loading="lazy" />
|
||
) : (
|
||
<span className="text-2xl font-bold text-[#666]">{e.company_name.charAt(0).toUpperCase()}</span>
|
||
)}
|
||
</div>
|
||
<span className="font-semibold text-[#e0e0e0] text-xs truncate w-full">{e.company_name}</span>
|
||
<span className="text-[9px] font-bold uppercase tracking-wider bg-emerald-100 text-emerald-300 px-1.5 py-0.5 rounded">Sponsor</span>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{/* Recent NAB-related stories */}
|
||
{articles.length > 0 && (
|
||
<section>
|
||
<header className="flex items-baseline justify-between mb-4">
|
||
<h2 className="text-xl font-display font-bold text-[#e0e0e0]">
|
||
Latest from the show floor
|
||
</h2>
|
||
<Link href="/news?search=NAB+2026" className="text-xs text-[#3b82f6] hover:underline">
|
||
See all NAB coverage →
|
||
</Link>
|
||
</header>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
{articles.slice(0, 9).map((a) => (
|
||
<Link
|
||
key={a.wp_slug}
|
||
href={`/news/${a.wp_slug}`}
|
||
className="group bg-[#0d0d0d] border border-[#252525] rounded-lg overflow-hidden hover:border-[#3b82f6] hover:shadow-sm transition-all"
|
||
>
|
||
{a.featured_image && (
|
||
/* eslint-disable-next-line @next/next/no-img-element */
|
||
<img
|
||
src={a.featured_image}
|
||
alt=""
|
||
className="w-full aspect-video object-cover group-hover:scale-[1.02] transition-transform"
|
||
loading="lazy"
|
||
/>
|
||
)}
|
||
<div className="p-3">
|
||
<h3 className="font-semibold text-[#e0e0e0] text-sm leading-snug group-hover:text-[#3b82f6] line-clamp-3 mb-1">
|
||
{a.title}
|
||
</h3>
|
||
{a.wp_published_at && (
|
||
<p className="text-[10px] text-[#888]">
|
||
{new Date(a.wp_published_at).toLocaleDateString("en-US", { month: "short", day: "numeric" })}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{/* Full exhibitor list */}
|
||
<section>
|
||
<header className="flex items-baseline justify-between mb-4">
|
||
<h2 className="text-xl font-display font-bold text-[#e0e0e0]">
|
||
All 2026 NAB Show exhibitors
|
||
</h2>
|
||
<Link href="/manufacturers" className="text-xs text-[#3b82f6] hover:underline">
|
||
Full directory →
|
||
</Link>
|
||
</header>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||
{otherRows.slice(0, 200).map((e) => (
|
||
<Link
|
||
key={e.slug}
|
||
href={`/manufacturers/${e.slug}`}
|
||
data-company-slug={e.slug}
|
||
className="bg-[#0d0d0d] border border-[#252525] rounded-md p-2.5 hover:border-[#3b82f6] transition-all flex items-center gap-2.5"
|
||
>
|
||
{e.logo_url ? (
|
||
/* eslint-disable-next-line @next/next/no-img-element */
|
||
<img src={e.logo_url} alt="" className="w-10 h-10 rounded bg-[#1a1a1a] object-contain flex-shrink-0" loading="lazy" />
|
||
) : (
|
||
<div className="w-10 h-10 rounded bg-[#1a1a1a] flex items-center justify-center text-[#666] font-bold text-sm flex-shrink-0">
|
||
{e.company_name.charAt(0).toUpperCase()}
|
||
</div>
|
||
)}
|
||
<span className="font-medium text-[#e0e0e0] text-xs truncate">{e.company_name}</span>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
{otherRows.length > 200 && (
|
||
<p className="text-center text-xs text-[#888] mt-4">
|
||
Showing 200 of {otherRows.length.toLocaleString()} exhibitors.{" "}
|
||
<Link href="/manufacturers" className="text-[#3b82f6] hover:underline">Browse the full directory →</Link>
|
||
</p>
|
||
)}
|
||
</section>
|
||
</div>
|
||
|
||
<Footer />
|
||
</div>
|
||
);
|
||
}
|