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:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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;
|
||||||
</span>
|
return (
|
||||||
)}
|
<span key={slug} className="text-[10px] font-semibold uppercase tracking-wider bg-[#1D4ED8]/10 text-[#1D4ED8] px-1.5 py-0.5 rounded">
|
||||||
{m.exhibits_ibc && (
|
{ev.short_name || ev.name}
|
||||||
<span className="text-[10px] font-semibold uppercase tracking-wider bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded">
|
</span>
|
||||||
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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user