fix: /manufacturers UI cleanup + /about/team phone formatting + /about media-kit link

Three pointed fixes:

/manufacturers (cloned from BB; still had broadcast vibe)
  • Drop the hardcoded "NAB + IBC" filter chip.
  • Per-card "2026 NAB Show" / "IBC 2026" badges replaced by dynamic
    pills generated from shows_attending intersected with av.events
    (so each card now shows the actual AV shows that exhibitor is
    attending — InfoComm, CEDIA, ISE, DSE, etc.).
  • Page intro counts: drop nabCount / ibcCount hardcoding, compute
    "top 3 shows by exhibitor count" from the data and surface those
    counts dynamically. With only InfoComm 2026 currently tagged the
    line reads "851 at InfoComm 2026" — adds more rows once other
    shows are scraped.
  • Meta description + OG: pro-AV / display / signage instead of
    "broadcast, post-production, and live-production."

/about/team phone formatting
  • New formatPhone() helper: E.164 → "(213) 418-2600" pattern.
  • Extension format: ", Ext. NNN" (capital E, comma-separated).
  • tel: link still uses the raw digits + RFC-3966 ',ext' pause for
    PBX auto-dial on supported clients.

/about page media-kit link
  • Was href="/about/advertise" — a relative path that 404s (no such
    route exists). Replaced with the absolute media-kit URL on
    relevantmediaproperties.com that the /advertise + /about/contact
    pages already use. Opens in a new tab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 14:53:54 +00:00
parent a63a5db6d3
commit 91e60caa60
4 changed files with 41 additions and 31 deletions

View File

@@ -57,7 +57,7 @@ export default function AboutPage() {
<Block title="Get involved"> <Block title="Get involved">
For editorial pitches: <a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="mailto:editor@avbeat.com">editor@avbeat.com</a>. For editorial pitches: <a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="mailto:editor@avbeat.com">editor@avbeat.com</a>.
{" "}For advertising: <a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="/about/advertise">see the media kit</a>. {" "}For advertising: <a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="https://relevantmediaproperties.com/av-beat---media-kit" target="_blank" rel="noopener noreferrer">see the media kit</a>.
{" "}For our team: <a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="/about/team">meet the editorial team</a>. {" "}For our team: <a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="/about/team">meet the editorial team</a>.
</Block> </Block>
</section> </section>

View File

@@ -77,6 +77,15 @@ export default async function TeamPage() {
.map((w) => w.charAt(0).toUpperCase()) .map((w) => w.charAt(0).toUpperCase())
.join(""); .join("");
// Format an E.164 US number as (XXX) XXX-XXXX. Falls back to the raw
// string for international or non-standard inputs.
const formatPhone = (raw: string): string => {
const digits = raw.replace(/[^0-9]/g, "");
const local = digits.startsWith("1") && digits.length === 11 ? digits.slice(1) : digits;
if (local.length !== 10) return raw;
return `(${local.slice(0, 3)}) ${local.slice(3, 6)}-${local.slice(6)}`;
};
// Derive @avbeat.com email from the persona slug. Real mailboxes // Derive @avbeat.com email from the persona slug. Real mailboxes
// are set up centrally; this is the authoring address surfaced to readers. // are set up centrally; this is the authoring address surfaced to readers.
const editorialEmail = (p: Persona): { user: string; domain: string } | null => { const editorialEmail = (p: Persona): { user: string; domain: string } | null => {
@@ -242,8 +251,8 @@ export default async function TeamPage() {
})()} })()}
{p.phone && ( {p.phone && (
<div> <div>
<a href={`tel:${p.phone.replace(/[^\d+]/g, '')}`} className="hover:text-[#7DD3FC] font-mono"> <a href={`tel:${p.phone.replace(/[^\d+]/g, '')}${p.extension ? ',' + p.extension : ''}`} className="hover:text-[#7DD3FC] font-mono">
{p.phone}{p.extension ? ` ext. ${p.extension}` : ''} {formatPhone(p.phone)}{p.extension ? `, Ext. ${p.extension}` : ''}
</a> </a>
</div> </div>
)} )}

View File

@@ -102,15 +102,6 @@ export default function ManufacturerDirectoryClient({
> >
All shows <span className="ml-1 opacity-60">({manufacturers.length.toLocaleString()})</span> All shows <span className="ml-1 opacity-60">({manufacturers.length.toLocaleString()})</span>
</button> </button>
<button
type="button"
onClick={() => setShowFilter("both-major")}
className={`px-3 py-1.5 rounded text-xs font-semibold ${
showFilter === "both-major" ? "bg-[#1D4ED8] text-white" : "bg-[#F8FAFC] border border-[#DCE6F2] text-[#aaa] hover:bg-[#FFFFFF]"
}`}
>
NAB + IBC <span className="ml-1 opacity-60">({manufacturers.filter((m) => m.exhibits_nab && m.exhibits_ibc).length})</span>
</button>
{shows.map((s) => { {shows.map((s) => {
const count = showCounts[s.slug] || 0; const count = showCounts[s.slug] || 0;
const active = showFilter === s.slug; const active = showFilter === s.slug;
@@ -184,18 +175,17 @@ export default function ManufacturerDirectoryClient({
{m.company_name} {m.company_name}
</h3> </h3>
<div className="flex items-center gap-1.5 mt-1 mb-1.5 flex-wrap"> <div className="flex items-center gap-1.5 mt-1 mb-1.5 flex-wrap">
{m.exhibits_nab && ( {(m.shows_attending || []).slice(0, 3).map((slug) => {
<span className="text-[10px] font-semibold uppercase tracking-wider bg-amber-50 text-amber-700 px-1.5 py-0.5 rounded"> const ev = shows.find((s) => s.slug === slug);
2026 NAB Show if (!ev) return null;
return (
<span key={slug} className="text-[10px] font-semibold uppercase tracking-wider bg-[#1D4ED8]/10 text-[#1D4ED8] px-1.5 py-0.5 rounded">
{ev.short_name || ev.name}
</span> </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 2026
</span>
)}
{m.featured && ( {m.featured && (
<span className="text-[10px] font-semibold uppercase tracking-wider bg-[#1D4ED8]/15 text-[#1D4ED8] px-1.5 py-0.5 rounded"> <span className="text-[10px] font-semibold uppercase tracking-wider bg-amber-50 text-amber-700 px-1.5 py-0.5 rounded">
Featured Featured
</span> </span>
)} )}

View File

@@ -10,12 +10,12 @@ export const revalidate = 1800;
export const metadata: Metadata = { export const metadata: Metadata = {
title: "AV Integration Manufacturers — AV Beat", title: "AV Integration Manufacturers — AV Beat",
description: 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.", "Browse the AV industry — pro AV, display, signage, live-production, and integration manufacturers exhibiting at InfoComm, CEDIA Expo, ISE, DSE, and more. Vendor profiles, booth numbers, product categories, and the latest news from each company.",
alternates: { canonical: "/manufacturers" }, alternates: { canonical: "/manufacturers" },
openGraph: { openGraph: {
title: "AV Integration Manufacturers — AV Beat", title: "AV Integration Manufacturers — AV Beat",
description: description:
"Comprehensive directory of broadcast industry manufacturers, exhibitors, and vendors.", "Comprehensive directory of pro AV, display, and integration manufacturers, exhibitors, and vendors.",
type: "website", type: "website",
url: "https://avbeat.com/manufacturers", url: "https://avbeat.com/manufacturers",
}, },
@@ -68,8 +68,19 @@ export default async function ManufacturersIndex() {
const list = (data || []) as ManufacturerRow[]; const list = (data || []) as ManufacturerRow[];
const events = (eventsData || []) as ShowOption[]; const events = (eventsData || []) as ShowOption[];
const nabCount = list.filter((m) => m.exhibits_nab).length; // Per-show exhibitor counts pulled from the same shows_attending array
const ibcCount = list.filter((m) => m.exhibits_ibc).length; // the DirectoryClient uses for its filter chips. Lets the intro line
// surface the BIGGEST show counts dynamically rather than hardcoding
// any particular event.
const showCounts = list.reduce<Record<string, number>>((m, row) => {
for (const s of row.shows_attending || []) m[s] = (m[s] || 0) + 1;
return m;
}, {});
const topShows = events
.map((e) => ({ event: e, count: showCounts[e.slug] || 0 }))
.filter((x) => x.count > 0)
.sort((a, b) => b.count - a.count)
.slice(0, 3);
return ( return (
<div className="min-h-screen bg-[#F8FAFC]"> <div className="min-h-screen bg-[#F8FAFC]">
@@ -80,10 +91,10 @@ export default async function ManufacturersIndex() {
AV Integration Manufacturers AV Integration Manufacturers
</h1> </h1>
<p className="text-[#475569] text-base max-w-3xl"> <p className="text-[#475569] text-base max-w-3xl">
{list.length.toLocaleString()} broadcast, post-production, and live-production {list.length.toLocaleString()} pro AV, display, signage, and integration manufacturers
manufacturers {topShows.map(({ event, count }) => (
{nabCount > 0 && <> · {nabCount.toLocaleString()} exhibiting at NAB Show 2026</>} <span key={event.slug}> · {count.toLocaleString()} at {event.short_name || event.name}</span>
{ibcCount > 0 && <> · {ibcCount.toLocaleString()} at IBC</>}. ))}.
</p> </p>
{error && ( {error && (
<p className="text-sm text-red-600 mt-2"> <p className="text-sm text-red-600 mt-2">