Two compounding bugs caused 0 clicks since May 22: 1. lib/ads.ts exported ADS_728X90 / ADS_300X250 / FIXED_300X600 as IIFE-evaluated module constants. They captured the FALLBACK array at module-load time, before the centralized RMP banner cache populates. Every render that referenced them got FALLBACK banners whose click_url was the direct advertiser URL — bypassing the centralized click logger entirely. 300x250s escaped because every page reads them via rotateAll() at render time. 2. Even when the new RMP-cache banners did render, click_url pointed at www.RMP.com/<property>/<slug>, which logs to adv.banner_clicks. The RMP analytics dashboard reads bb.ad_clicks. Repointed click URLs at advertising.RMP.com/r/<campaign_id> so all clicks land in the single table the dashboard queries. Files: lib/ads.ts (remove broken IIFE exports + repoint click_url), SidebarAdStack, Header, Footer, and 9 pages (reading-list, forum, forum/thread, search, gear, technology, authors, show-coverage x2). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
226 lines
10 KiB
TypeScript
226 lines
10 KiB
TypeScript
import React from "react";
|
|
import type { Metadata } from "next";
|
|
import Link from "next/link";
|
|
import Header from "@/components/Header";
|
|
import Footer from "@/components/Footer";
|
|
import AppImage from "@/components/ui/AppImage";
|
|
import AdImage from "@/components/AdImage";
|
|
import { pickAds } from "@/lib/ads";
|
|
import { getLegacyArticlesBySection } from "@/lib/articles/legacy-source";
|
|
import SidebarAdStack from "@/components/SidebarAdStack";
|
|
|
|
export const revalidate = 1800;
|
|
|
|
// Category sub-filters keyed off the ?category= query string. Each one
|
|
// pairs a display title with the keywords used to match an article's
|
|
// category, tags, or title (substring, case-insensitive).
|
|
const CATEGORY_FILTERS: Record<string, { title: string; keywords: string[]; description: string }> = {
|
|
ai: {
|
|
title: "AI & Automation",
|
|
keywords: ["ai", "artificial intelligence", "automation", "machine learning", "ml"],
|
|
description: "Coverage of AI and automation in broadcast — generative tools, ML-driven workflows, automated playout, and editorial-grade AI.",
|
|
},
|
|
cloud: {
|
|
title: "Cloud Production",
|
|
keywords: ["cloud"],
|
|
description: "Cloud-native production, remote workflows, hybrid pipelines, and SaaS-delivered broadcast infrastructure.",
|
|
},
|
|
"ip-workflows": {
|
|
title: "IP Workflows",
|
|
keywords: ["ip", "st 2110", "smpte 2110", "nmos"],
|
|
description: "ST 2110, NMOS, IP infrastructure, and the migration off SDI.",
|
|
},
|
|
streaming: {
|
|
title: "Streaming",
|
|
keywords: ["streaming", "ott", "fast", "vod"],
|
|
description: "OTT, FAST, VOD, low-latency streaming, and direct-to-consumer delivery.",
|
|
},
|
|
"atsc-3": {
|
|
title: "ATSC 3.0 / NextGen TV",
|
|
keywords: ["atsc", "nextgen"],
|
|
description: "ATSC 3.0 deployments, 5G Broadcast, datacasting, and the U.S. terrestrial transition.",
|
|
},
|
|
};
|
|
|
|
interface PageProps {
|
|
searchParams?: Promise<{ category?: string }>;
|
|
}
|
|
|
|
export async function generateMetadata({ searchParams }: PageProps): Promise<Metadata> {
|
|
const sp = await searchParams;
|
|
const cat = sp?.category && CATEGORY_FILTERS[sp.category];
|
|
const title = cat ? `${cat.title} — Broadcast Beat` : "Production Technology Deep Dives — Broadcast Beat";
|
|
const desc = cat?.description || "Deep-dive analysis of broadcast technology trends: IP workflows, cloud production, AI automation, streaming infrastructure, and ATSC 3.0.";
|
|
return {
|
|
title,
|
|
description: desc,
|
|
alternates: { canonical: sp?.category ? `/technology?category=${sp.category}` : "/technology" },
|
|
openGraph: { title, description: desc, url: "/technology", type: "website" },
|
|
};
|
|
}
|
|
|
|
function articleMatches(a: { title?: string | null; category?: string | null; tags?: string[] | null }, keywords: string[]): boolean {
|
|
const hay = `${a.title || ""} ${a.category || ""} ${(a.tags || []).join(" ")}`.toLowerCase();
|
|
return keywords.some((k) => hay.includes(k.toLowerCase()));
|
|
}
|
|
|
|
export default async function TechnologyPage({ searchParams }: PageProps) {
|
|
const sp = await searchParams;
|
|
const filterSlug = sp?.category && CATEGORY_FILTERS[sp.category] ? sp.category : null;
|
|
const filter = filterSlug ? CATEGORY_FILTERS[filterSlug] : null;
|
|
|
|
const baseArticles = await getLegacyArticlesBySection("technology");
|
|
const articles = filter ? baseArticles.filter((a) => articleMatches(a, filter.keywords)) : baseArticles;
|
|
|
|
const h1 = filter ? filter.title : "Production Technology";
|
|
const subtitle = filter
|
|
? filter.description
|
|
: "Deep-dive coverage of IP infrastructure, AI, cloud production, and the standards shaping broadcast's future";
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background">
|
|
<Header />
|
|
{/* DO NOT OVERRIDE — Technology page hero header */}
|
|
<div className="bg-[#111] border-b border-[#222] py-8">
|
|
<div className="max-w-container mx-auto px-4">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<span className="section-label">Technology</span>
|
|
<div className="flex-1 h-px bg-[#2a2a2a]" />
|
|
{filter && (
|
|
<Link href="/technology" className="text-[#666] hover:text-[#3b82f6] font-body text-[11px] underline">
|
|
clear filter
|
|
</Link>
|
|
)}
|
|
</div>
|
|
<h1 className="font-heading text-white text-3xl font-bold">{h1}</h1>
|
|
<p className="text-[#777] font-body text-sm mt-2">{subtitle}</p>
|
|
|
|
{/* Sub-category chips */}
|
|
<div className="flex items-center gap-2 mt-5 flex-wrap">
|
|
<Link
|
|
href="/technology"
|
|
className={`px-3 py-1.5 rounded-sm font-body text-[11px] font-semibold uppercase tracking-wide transition-all ${
|
|
!filterSlug
|
|
? "bg-[#3b82f6]/20 text-[#3b82f6] border border-[#3b82f6]/50"
|
|
: "bg-[#111] text-[#888] border border-[#2a2a2a] hover:text-[#ccc]"
|
|
}`}>
|
|
All
|
|
</Link>
|
|
{Object.entries(CATEGORY_FILTERS).map(([slug, def]) => {
|
|
const isActive = filterSlug === slug;
|
|
return (
|
|
<Link
|
|
key={slug}
|
|
href={`/technology?category=${slug}`}
|
|
className={`px-3 py-1.5 rounded-sm font-body text-[11px] font-semibold uppercase tracking-wide transition-all ${
|
|
isActive
|
|
? "bg-[#3b82f6]/20 text-[#3b82f6] border border-[#3b82f6]/50"
|
|
: "bg-[#111] text-[#888] border border-[#2a2a2a] hover:text-[#ccc]"
|
|
}`}>
|
|
{def.title}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<main className="max-w-container mx-auto px-4 py-8">
|
|
<aside className="float-right ml-6 mb-6 hidden lg:block max-w-[300px]"><SidebarAdStack /></aside>
|
|
{articles.length === 0 && (
|
|
<div className="text-center py-16">
|
|
<p className="font-body text-[#888] text-base">
|
|
No articles found{filter ? ` for "${filter.title}"` : ""}.
|
|
</p>
|
|
{filter && (
|
|
<Link href="/technology" className="inline-block mt-3 text-[#3b82f6] hover:underline font-body text-sm">
|
|
← Back to all technology coverage
|
|
</Link>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Featured technology article */}
|
|
{articles[0] && (
|
|
<div className="mb-10">
|
|
<Link
|
|
href={`/articles/${articles[0].slug}`}
|
|
className="flex flex-col md:flex-row gap-6 bg-[#111] border border-[#222] overflow-hidden group hover:border-[#3b82f6] transition-colors p-5">
|
|
<div className="flex-shrink-0 relative w-full md:w-[320px] h-[200px] md:h-[200px] overflow-hidden rounded-sm">
|
|
<AppImage
|
|
src={articles[0].image}
|
|
alt={articles[0].alt}
|
|
fill
|
|
className="object-cover group-hover:scale-105 transition-transform duration-300"
|
|
sizes="(max-width: 768px) 100vw, 320px"
|
|
/>
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<span className="text-[#3b82f6] font-body text-[10px] font-bold uppercase tracking-wider">{articles[0].category}</span>
|
|
<span className="text-[#444] text-[10px]">·</span>
|
|
<span className="text-[#555] font-body text-[11px]">{articles[0].date}</span>
|
|
<span className="text-[#444] text-[10px]">·</span>
|
|
<span className="text-[#555] font-body text-[11px]">{articles[0].readTime}</span>
|
|
</div>
|
|
<h2 className="font-heading text-[#e0e0e0] text-xl font-bold leading-tight mb-3 group-hover:text-[#3b82f6] transition-colors">
|
|
{articles[0].title}
|
|
</h2>
|
|
<p className="text-[#777] font-body text-sm leading-relaxed mb-4">{articles[0].excerpt}</p>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{articles[0].tags.map((tag) => (
|
|
<span key={tag} className="px-2 py-0.5 bg-[#1a1a1a] border border-[#2a2a2a] text-[#666] text-[10px] font-body rounded-sm">
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
</div>
|
|
)}
|
|
|
|
{/* Technology article grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{articles.slice(1).map((article) => (
|
|
<Link
|
|
key={article.slug}
|
|
href={`/articles/${article.slug}`}
|
|
className="flex gap-4 bg-[#111] border border-[#222] p-4 group hover:border-[#3b82f6] transition-colors">
|
|
<div className="flex-shrink-0 w-[120px] h-[80px] relative overflow-hidden rounded-sm">
|
|
<AppImage
|
|
src={article.image}
|
|
alt={article.alt}
|
|
fill
|
|
className="object-cover group-hover:scale-105 transition-transform duration-300"
|
|
sizes="120px"
|
|
/>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className="text-[#3b82f6] font-body text-[9px] font-bold uppercase tracking-wider">{article.category}</span>
|
|
<span className="text-[#444] text-[9px]">·</span>
|
|
<span className="text-[#555] font-body text-[10px]">{article.readTime}</span>
|
|
</div>
|
|
<h2 className="font-heading text-[#e0e0e0] text-sm font-bold leading-snug mb-1 group-hover:text-[#3b82f6] transition-colors line-clamp-2">
|
|
{article.title}
|
|
</h2>
|
|
{article.category !== "Featured" && (
|
|
<span className="text-[#555] font-body text-[11px]">{article.date}</span>
|
|
)}
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
|
|
{/* In-content 728x90 banner */}
|
|
{pickAds("728x90", 1).length > 0 && (
|
|
<div className="mt-12 hidden md:flex justify-center" aria-label="Advertisement">
|
|
<AdImage ad={pickAds("728x90", 1)[0]} page="technology" />
|
|
</div>
|
|
)}
|
|
</main>
|
|
<Footer />
|
|
</div>
|
|
);
|
|
}
|