manufacturers: public directory with 1,142 NAB 2026 exhibitors
New public routes: /manufacturers - searchable index, filter by show + first letter /manufacturers/[slug] - profile page with bio, categories, trade shows, news, JSON-LD Data sourced from bb.tracked_companies + bb.trade_show_exhibitors, seeded from the NAB Show 2026 Map Your Show exhibitor list. Adds schema extensions on bb.tracked_companies (slug, logo_url, bio, contact, social handles, press_url, ad_status, exhibits_nab/ibc) and bb.press_releases (manufacturer_id FK, source_url, published_at, status). Each profile page emits Organization JSON-LD for SEO + canonical URL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
161
src/app/manufacturers/DirectoryClient.tsx
Normal file
161
src/app/manufacturers/DirectoryClient.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
type Manufacturer = {
|
||||||
|
slug: string;
|
||||||
|
company_name: string;
|
||||||
|
bio: string | null;
|
||||||
|
logo_url: string | null;
|
||||||
|
categories: string[] | null;
|
||||||
|
exhibits_nab: boolean;
|
||||||
|
exhibits_ibc: boolean;
|
||||||
|
mention_count: number;
|
||||||
|
featured: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SHOW_FILTERS = ["All", "NAB Show", "IBC", "Both shows"] as const;
|
||||||
|
type ShowFilter = (typeof SHOW_FILTERS)[number];
|
||||||
|
|
||||||
|
export default function ManufacturerDirectoryClient({
|
||||||
|
manufacturers,
|
||||||
|
}: {
|
||||||
|
manufacturers: Manufacturer[];
|
||||||
|
}) {
|
||||||
|
const [q, setQ] = useState("");
|
||||||
|
const [showFilter, setShowFilter] = useState<ShowFilter>("All");
|
||||||
|
const [letter, setLetter] = useState<string>("All");
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const needle = q.trim().toLowerCase();
|
||||||
|
return manufacturers.filter((m) => {
|
||||||
|
if (needle) {
|
||||||
|
const hay = `${m.company_name} ${m.bio || ""}`.toLowerCase();
|
||||||
|
if (!hay.includes(needle)) return false;
|
||||||
|
}
|
||||||
|
if (showFilter === "NAB Show" && !m.exhibits_nab) return false;
|
||||||
|
if (showFilter === "IBC" && !m.exhibits_ibc) return false;
|
||||||
|
if (showFilter === "Both shows" && !(m.exhibits_nab && m.exhibits_ibc)) return false;
|
||||||
|
if (letter !== "All") {
|
||||||
|
const c = (m.company_name || "").trim().charAt(0).toUpperCase();
|
||||||
|
if (letter === "#") {
|
||||||
|
if (/^[A-Z]/.test(c)) return false;
|
||||||
|
} else if (c !== letter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [manufacturers, q, showFilter, letter]);
|
||||||
|
|
||||||
|
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="bg-surface-soft border border-ink/10 rounded-xl p-4 mb-6 flex flex-col gap-3 md:flex-row md:items-center md:gap-4">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
value={q}
|
||||||
|
onChange={(e) => setQ(e.target.value)}
|
||||||
|
placeholder="Search by company or product…"
|
||||||
|
className="w-full bg-white border border-ink/15 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-primary/40"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1 bg-white border border-ink/10 rounded-md p-1">
|
||||||
|
{SHOW_FILTERS.map((f) => (
|
||||||
|
<button
|
||||||
|
key={f}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowFilter(f)}
|
||||||
|
className={`px-3 py-1.5 rounded text-xs font-semibold ${
|
||||||
|
showFilter === f ? "bg-brand-primary text-white" : "text-ink/70 hover:bg-ink/5"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{f}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="flex flex-wrap gap-1 mb-6 text-xs">
|
||||||
|
{(["All", "#", ...letters] as const).map((l) => (
|
||||||
|
<button
|
||||||
|
key={l}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setLetter(l)}
|
||||||
|
className={`min-w-[28px] h-7 px-1.5 rounded font-semibold ${
|
||||||
|
letter === l
|
||||||
|
? "bg-brand-primary text-white"
|
||||||
|
: "bg-white border border-ink/10 text-ink/65 hover:bg-ink/5"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{l}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<p className="text-xs text-ink/55 mb-3">
|
||||||
|
Showing {filtered.length.toLocaleString()} of {manufacturers.length.toLocaleString()} manufacturers
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{filtered.map((m) => (
|
||||||
|
<Link
|
||||||
|
key={m.slug}
|
||||||
|
href={`/manufacturers/${m.slug}`}
|
||||||
|
className="group bg-white border border-ink/10 rounded-xl p-4 hover:border-brand-primary hover:shadow-sm transition-all flex gap-3"
|
||||||
|
>
|
||||||
|
<div className="w-14 h-14 rounded-lg bg-ink/5 flex items-center justify-center overflow-hidden flex-shrink-0">
|
||||||
|
{m.logo_url ? (
|
||||||
|
/* eslint-disable-next-line @next/next/no-img-element */
|
||||||
|
<img
|
||||||
|
src={m.logo_url}
|
||||||
|
alt={`${m.company_name} logo`}
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-ink/40 text-lg font-semibold">
|
||||||
|
{(m.company_name || "?").trim().charAt(0).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h3 className="font-semibold text-ink truncate group-hover:text-brand-primary transition-colors">
|
||||||
|
{m.company_name}
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-1.5 mt-1 mb-1.5 flex-wrap">
|
||||||
|
{m.exhibits_nab && (
|
||||||
|
<span className="text-[10px] font-semibold uppercase tracking-wider bg-amber-50 text-amber-700 px-1.5 py-0.5 rounded">
|
||||||
|
NAB 26
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{m.exhibits_ibc && (
|
||||||
|
<span className="text-[10px] font-semibold uppercase tracking-wider bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded">
|
||||||
|
IBC
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{m.featured && (
|
||||||
|
<span className="text-[10px] font-semibold uppercase tracking-wider bg-brand-primary/10 text-brand-primary px-1.5 py-0.5 rounded">
|
||||||
|
Featured
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-ink/60 line-clamp-2">
|
||||||
|
{m.bio || "Profile coming soon."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filtered.length === 0 && (
|
||||||
|
<div className="text-center py-12 text-ink/50 text-sm">
|
||||||
|
No manufacturers match these filters.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
298
src/app/manufacturers/[slug]/page.tsx
Normal file
298
src/app/manufacturers/[slug]/page.tsx
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Header from "@/components/Header";
|
||||||
|
import Footer from "@/components/Footer";
|
||||||
|
import { createClient } from "@/lib/supabase/server";
|
||||||
|
|
||||||
|
export const revalidate = 1800;
|
||||||
|
|
||||||
|
type Params = { slug: string };
|
||||||
|
|
||||||
|
async function loadManufacturer(slug: string) {
|
||||||
|
const supabase = await createClient();
|
||||||
|
const { data } = await supabase
|
||||||
|
.from("tracked_companies")
|
||||||
|
.select(
|
||||||
|
"id, slug, company_name, bio, bio_source, logo_url, company_website, contact_email, contact_url, hq_city, hq_country, linkedin_url, twitter_handle, youtube_url, press_url, categories, exhibits_nab, exhibits_ibc, mention_count, last_mentioned, featured, ad_status"
|
||||||
|
)
|
||||||
|
.eq("slug", slug)
|
||||||
|
.eq("directory_visible", true)
|
||||||
|
.maybeSingle();
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
// Per-show appearances
|
||||||
|
const { data: shows } = await supabase
|
||||||
|
.from("trade_show_exhibitors")
|
||||||
|
.select("show, show_year, booth, description, url, website")
|
||||||
|
.eq("exhibitor_name", data.company_name)
|
||||||
|
.order("show_year", { ascending: false });
|
||||||
|
|
||||||
|
// Recent BB stories that mention this company (mention_count guides interest)
|
||||||
|
const { data: pressReleases } = await supabase
|
||||||
|
.from("press_releases")
|
||||||
|
.select("title, slug, published_at, source_url")
|
||||||
|
.eq("manufacturer_id", data.id)
|
||||||
|
.order("published_at", { ascending: false })
|
||||||
|
.limit(10);
|
||||||
|
|
||||||
|
return { company: data, shows: shows || [], pressReleases: pressReleases || [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<Params>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
|
const { slug } = await params;
|
||||||
|
const result = await loadManufacturer(slug);
|
||||||
|
if (!result) {
|
||||||
|
return { title: "Manufacturer not found — Broadcast Beat" };
|
||||||
|
}
|
||||||
|
const { company } = result;
|
||||||
|
const title = `${company.company_name} — Manufacturer Profile | Broadcast Beat`;
|
||||||
|
const description =
|
||||||
|
(company.bio || "").slice(0, 200) ||
|
||||||
|
`${company.company_name} is a broadcast industry manufacturer covered by Broadcast Beat.`;
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
alternates: { canonical: `/manufacturers/${slug}` },
|
||||||
|
openGraph: {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
url: `https://broadcastbeat.com/manufacturers/${slug}`,
|
||||||
|
type: "profile",
|
||||||
|
...(company.logo_url ? { images: [{ url: company.logo_url }] } : {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ManufacturerProfile({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<Params>;
|
||||||
|
}) {
|
||||||
|
const { slug } = await params;
|
||||||
|
const result = await loadManufacturer(slug);
|
||||||
|
if (!result) notFound();
|
||||||
|
const { company, shows, pressReleases } = result;
|
||||||
|
|
||||||
|
const jsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Organization",
|
||||||
|
name: company.company_name,
|
||||||
|
description: company.bio || undefined,
|
||||||
|
url: company.company_website || undefined,
|
||||||
|
logo: company.logo_url || undefined,
|
||||||
|
sameAs: [
|
||||||
|
company.linkedin_url,
|
||||||
|
company.twitter_handle ? `https://twitter.com/${company.twitter_handle}` : null,
|
||||||
|
company.youtube_url,
|
||||||
|
].filter(Boolean),
|
||||||
|
address:
|
||||||
|
company.hq_city || company.hq_country
|
||||||
|
? {
|
||||||
|
"@type": "PostalAddress",
|
||||||
|
addressLocality: company.hq_city || undefined,
|
||||||
|
addressCountry: company.hq_country || undefined,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-white">
|
||||||
|
<Header />
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
|
<article className="max-w-4xl mx-auto px-4 py-10">
|
||||||
|
<nav className="text-xs text-ink/55 mb-6">
|
||||||
|
<Link href="/manufacturers" className="hover:text-brand-primary">
|
||||||
|
← Manufacturer Directory
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header className="flex flex-col sm:flex-row gap-6 mb-8 pb-8 border-b border-ink/10">
|
||||||
|
<div className="w-24 h-24 rounded-xl bg-ink/5 flex items-center justify-center overflow-hidden flex-shrink-0">
|
||||||
|
{company.logo_url ? (
|
||||||
|
/* eslint-disable-next-line @next/next/no-img-element */
|
||||||
|
<img
|
||||||
|
src={company.logo_url}
|
||||||
|
alt={`${company.company_name} logo`}
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-ink/40 text-3xl font-bold">
|
||||||
|
{(company.company_name || "?").trim().charAt(0).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h1 className="text-3xl md:text-4xl font-display font-bold text-ink mb-1">
|
||||||
|
{company.company_name}
|
||||||
|
</h1>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap text-xs">
|
||||||
|
{company.exhibits_nab && (
|
||||||
|
<span className="bg-amber-50 text-amber-700 px-2 py-0.5 rounded font-semibold uppercase tracking-wider">
|
||||||
|
NAB Show 2026
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{company.exhibits_ibc && (
|
||||||
|
<span className="bg-blue-50 text-blue-700 px-2 py-0.5 rounded font-semibold uppercase tracking-wider">
|
||||||
|
IBC
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{(company.hq_city || company.hq_country) && (
|
||||||
|
<span className="text-ink/60">
|
||||||
|
{[company.hq_city, company.hq_country].filter(Boolean).join(", ")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex flex-wrap gap-3 text-sm">
|
||||||
|
{company.company_website && (
|
||||||
|
<a
|
||||||
|
href={company.company_website}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-brand-primary hover:underline"
|
||||||
|
>
|
||||||
|
Website ↗
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{company.press_url && (
|
||||||
|
<a
|
||||||
|
href={company.press_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-brand-primary hover:underline"
|
||||||
|
>
|
||||||
|
Newsroom ↗
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{company.contact_url && (
|
||||||
|
<a
|
||||||
|
href={company.contact_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-brand-primary hover:underline"
|
||||||
|
>
|
||||||
|
Contact ↗
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{company.linkedin_url && (
|
||||||
|
<a
|
||||||
|
href={company.linkedin_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-ink/60 hover:text-brand-primary"
|
||||||
|
>
|
||||||
|
LinkedIn
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{company.twitter_handle && (
|
||||||
|
<a
|
||||||
|
href={`https://twitter.com/${company.twitter_handle}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-ink/60 hover:text-brand-primary"
|
||||||
|
>
|
||||||
|
X / Twitter
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{company.bio && (
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-lg font-display font-bold text-ink mb-2">About</h2>
|
||||||
|
<p className="text-ink/80 leading-relaxed">{company.bio}</p>
|
||||||
|
{company.bio_source && (
|
||||||
|
<p className="text-xs text-ink/40 mt-2">
|
||||||
|
Source: {company.bio_source}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{company.categories && company.categories.length > 0 && (
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-lg font-display font-bold text-ink mb-2">Categories</h2>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{company.categories.map((c: string) => (
|
||||||
|
<span
|
||||||
|
key={c}
|
||||||
|
className="text-xs bg-ink/5 text-ink/70 px-2.5 py-1 rounded-full"
|
||||||
|
>
|
||||||
|
{c}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{shows.length > 0 && (
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-lg font-display font-bold text-ink mb-3">
|
||||||
|
Trade-show appearances
|
||||||
|
</h2>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{shows.map((s) => (
|
||||||
|
<li
|
||||||
|
key={`${s.show}-${s.show_year}-${s.booth || "?"}`}
|
||||||
|
className="bg-surface-soft border border-ink/10 rounded-md px-4 py-3 flex items-center justify-between gap-4"
|
||||||
|
>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<span className="font-semibold text-ink">
|
||||||
|
{s.show} {s.show_year}
|
||||||
|
</span>
|
||||||
|
{s.booth && (
|
||||||
|
<span className="ml-3 text-sm text-ink/60">Booth {s.booth}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pressReleases.length > 0 && (
|
||||||
|
<section className="mb-10">
|
||||||
|
<h2 className="text-lg font-display font-bold text-ink mb-3">Recent news</h2>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{pressReleases.map((p) => (
|
||||||
|
<li key={p.slug || p.source_url} className="text-sm">
|
||||||
|
{p.slug ? (
|
||||||
|
<Link
|
||||||
|
href={`/news/${p.slug}`}
|
||||||
|
className="text-brand-primary hover:underline"
|
||||||
|
>
|
||||||
|
{p.title}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span>{p.title}</span>
|
||||||
|
)}
|
||||||
|
{p.published_at && (
|
||||||
|
<span className="text-ink/50 text-xs ml-2">
|
||||||
|
{new Date(p.published_at).toISOString().slice(0, 10)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<footer className="text-xs text-ink/40 mt-12 pt-6 border-t border-ink/10">
|
||||||
|
Listing maintained by Broadcast Beat editorial.
|
||||||
|
{company.last_mentioned && (
|
||||||
|
<> Last mentioned in coverage on {new Date(company.last_mentioned).toISOString().slice(0, 10)}.</>
|
||||||
|
)}
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
95
src/app/manufacturers/page.tsx
Normal file
95
src/app/manufacturers/page.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
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 ManufacturerDirectoryClient from "./DirectoryClient";
|
||||||
|
|
||||||
|
export const revalidate = 1800;
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Manufacturer Directory — Broadcast Beat",
|
||||||
|
description:
|
||||||
|
"Browse 1,000+ broadcast, post-production, and live-production manufacturers exhibiting at NAB Show and IBC. Find vendor profiles, booth numbers, product categories, and the latest news from each company.",
|
||||||
|
alternates: { canonical: "/manufacturers" },
|
||||||
|
openGraph: {
|
||||||
|
title: "Broadcast Manufacturer Directory",
|
||||||
|
description:
|
||||||
|
"Comprehensive directory of broadcast industry manufacturers, exhibitors, and vendors.",
|
||||||
|
type: "website",
|
||||||
|
url: "https://broadcastbeat.com/manufacturers",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type ManufacturerRow = {
|
||||||
|
slug: string;
|
||||||
|
company_name: string;
|
||||||
|
bio: string | null;
|
||||||
|
logo_url: string | null;
|
||||||
|
categories: string[] | null;
|
||||||
|
exhibits_nab: boolean;
|
||||||
|
exhibits_ibc: boolean;
|
||||||
|
mention_count: number;
|
||||||
|
featured: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function ManufacturersIndex() {
|
||||||
|
const supabase = await createClient();
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("tracked_companies")
|
||||||
|
.select(
|
||||||
|
"slug, company_name, bio, logo_url, categories, exhibits_nab, exhibits_ibc, mention_count, featured"
|
||||||
|
)
|
||||||
|
.eq("directory_visible", true)
|
||||||
|
.not("slug", "is", null)
|
||||||
|
.order("featured", { ascending: false })
|
||||||
|
.order("mention_count", { ascending: false })
|
||||||
|
.order("company_name", { ascending: true })
|
||||||
|
.limit(2000);
|
||||||
|
|
||||||
|
const list = (data || []) as ManufacturerRow[];
|
||||||
|
const nabCount = list.filter((m) => m.exhibits_nab).length;
|
||||||
|
const ibcCount = list.filter((m) => m.exhibits_ibc).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-white">
|
||||||
|
<Header />
|
||||||
|
<div className="max-w-6xl mx-auto px-4 py-10">
|
||||||
|
<header className="mb-8">
|
||||||
|
<h1 className="text-3xl md:text-4xl font-display font-bold text-ink mb-2">
|
||||||
|
Manufacturer Directory
|
||||||
|
</h1>
|
||||||
|
<p className="text-ink/70 text-base max-w-3xl">
|
||||||
|
{list.length.toLocaleString()} broadcast, post-production, and live-production
|
||||||
|
manufacturers
|
||||||
|
{nabCount > 0 && <> · {nabCount.toLocaleString()} exhibiting at NAB Show 2026</>}
|
||||||
|
{ibcCount > 0 && <> · {ibcCount.toLocaleString()} at IBC</>}.
|
||||||
|
</p>
|
||||||
|
{error && (
|
||||||
|
<p className="text-sm text-red-600 mt-2">
|
||||||
|
Note: error loading directory ({error.message})
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<ManufacturerDirectoryClient manufacturers={list} />
|
||||||
|
|
||||||
|
<section className="mt-16 border-t border-ink/10 pt-8 text-sm text-ink/55 leading-relaxed">
|
||||||
|
<p>
|
||||||
|
Sources: 2026 NAB Show exhibitor list (Map Your Show), IBC exhibitor list,
|
||||||
|
manufacturer-direct press releases, and reporting tracked on Broadcast Beat.
|
||||||
|
New manufacturers and updates land here as our crawlers detect them.
|
||||||
|
</p>
|
||||||
|
<p className="mt-2">
|
||||||
|
Are you a manufacturer and want to update your listing?{" "}
|
||||||
|
<Link href="/contact" className="text-brand-primary hover:underline">
|
||||||
|
Contact our editors
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user