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"; import BackLink from "./BackLink"; export const revalidate = 1800; type Params = { slug: string }; // Known show date ranges. Booth numbers only render when the show is // upcoming within 90 days; otherwise the row is shown without a booth. const SHOW_DATES: Record = { "NAB Show:2026": { start: "2026-04-11", end: "2026-04-16" }, "NAB Show NY:2026": { start: "2026-10-21", end: "2026-10-23" }, "IBC:2026": { start: "2026-09-11", end: "2026-09-14" }, "NAB Show:2027": { start: "2027-04-17", end: "2027-04-22" }, }; function normaliseShowName(raw: string | null | undefined): string { const s = (raw || "").trim(); if (!s) return s; if (/^nab(\s*show)?$/i.test(s)) return "NAB Show"; if (/^nab\s*(show\s*)?(ny|new\s*york)$/i.test(s)) return "NAB Show NY"; if (/^ibc(\s*show)?$/i.test(s)) return "IBC"; return s; } function shouldShowBooth(show: string, year: number | null): boolean { if (!year) return false; const dates = SHOW_DATES[`${show}:${year}`]; if (!dates) return false; const today = new Date(); const start = new Date(dates.start + "T00:00:00Z"); const end = new Date(dates.end + "T23:59:59Z"); if (today > end) return false; const daysUntilStart = (start.getTime() - today.getTime()) / 86_400_000; return daysUntilStart <= 90; } 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, phone, hq_address, 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; 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 }); 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); const { data: executives } = await supabase .from("manufacturer_executives") .select("id, name, title, photo_url, linkedin_url, bio") .eq("company_id", data.id) .order("sort_order") .order("name"); // Recent stories: title contains the company name. The same heuristic the // article auto-linker and the hover card use — keeps the directory in sync // with the editorial coverage even when manufacturer_id isn't backfilled. const needle = String(data.company_name || "").trim(); let recentStories: any[] = []; if (needle.length >= 3) { const { data: stories } = await supabase .from("wp_imported_posts") .select("wp_slug, title, excerpt, featured_image, wp_published_at, category") .eq("status", "published") .ilike("title", `%${needle.replace(/[%_]/g, "")}%`) .order("wp_published_at", { ascending: false, nullsFirst: false }) .limit(12); recentStories = stories || []; } return { company: data, shows: shows || [], pressReleases: pressReleases || [], executives: executives || [], recentStories, }; } export async function generateMetadata({ params, }: { params: Promise; }): Promise { const { slug } = await params; const result = await loadManufacturer(slug); if (!result) { return { title: "Manufacturer not found — AV Beat" }; } const { company } = result; const title = `${company.company_name} — Manufacturer Profile | AV Beat`; const description = (company.bio || "").slice(0, 200) || `${company.company_name} is a broadcast industry manufacturer covered by AV Beat.`; return { title, description, alternates: { canonical: `/manufacturers/${slug}` }, openGraph: { title, description, url: `https://avbeat.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, executives, recentStories } = 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 (