From 0e733ca0f8903a75adf20bc61ee68e1a9a25a1fd Mon Sep 17 00:00:00 2001 From: Ryan Salazar Date: Tue, 26 May 2026 14:18:16 +0000 Subject: [PATCH] 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) --- src/app/manufacturers/DirectoryClient.tsx | 161 ++++++++++++ src/app/manufacturers/[slug]/page.tsx | 298 ++++++++++++++++++++++ src/app/manufacturers/page.tsx | 95 +++++++ 3 files changed, 554 insertions(+) create mode 100644 src/app/manufacturers/DirectoryClient.tsx create mode 100644 src/app/manufacturers/[slug]/page.tsx create mode 100644 src/app/manufacturers/page.tsx diff --git a/src/app/manufacturers/DirectoryClient.tsx b/src/app/manufacturers/DirectoryClient.tsx new file mode 100644 index 0000000..07500d8 --- /dev/null +++ b/src/app/manufacturers/DirectoryClient.tsx @@ -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("All"); + const [letter, setLetter] = useState("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 ( +
+
+
+ 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" + /> +
+
+ {SHOW_FILTERS.map((f) => ( + + ))} +
+
+ + + +

+ Showing {filtered.length.toLocaleString()} of {manufacturers.length.toLocaleString()} manufacturers +

+ +
+ {filtered.map((m) => ( + +
+ {m.logo_url ? ( + /* eslint-disable-next-line @next/next/no-img-element */ + {`${m.company_name} + ) : ( + + {(m.company_name || "?").trim().charAt(0).toUpperCase()} + + )} +
+
+

+ {m.company_name} +

+
+ {m.exhibits_nab && ( + + NAB 26 + + )} + {m.exhibits_ibc && ( + + IBC + + )} + {m.featured && ( + + Featured + + )} +
+

+ {m.bio || "Profile coming soon."} +

+
+ + ))} +
+ + {filtered.length === 0 && ( +
+ No manufacturers match these filters. +
+ )} +
+ ); +} diff --git a/src/app/manufacturers/[slug]/page.tsx b/src/app/manufacturers/[slug]/page.tsx new file mode 100644 index 0000000..0069dfa --- /dev/null +++ b/src/app/manufacturers/[slug]/page.tsx @@ -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; +}): Promise { + 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; +}) { + 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 ( +
+
+