ads: route all clicks through advertising.RMP.com/r/<id> + cache-aware banner reads

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>
This commit is contained in:
Ryan Salazar
2026-05-28 02:07:06 +00:00
parent 95eba5794b
commit ed764b1bbe
13 changed files with 52 additions and 45 deletions

View File

@@ -6,7 +6,7 @@ import AppImage from "@/components/ui/AppImage";
import Header from "@/components/Header"; import Header from "@/components/Header";
import Footer from "@/components/Footer"; import Footer from "@/components/Footer";
import AdImage from "@/components/AdImage"; import AdImage from "@/components/AdImage";
import { ADS_728X90, shuffle } from "@/lib/ads"; import { pickAds } from "@/lib/ads";
import SidebarAdStack from "@/components/SidebarAdStack"; import SidebarAdStack from "@/components/SidebarAdStack";
interface AuthorProfile { interface AuthorProfile {

View File

@@ -5,7 +5,7 @@ import Header from '@/components/Header';
import Footer from '@/components/Footer'; import Footer from '@/components/Footer';
import AdImage from '@/components/AdImage'; import AdImage from '@/components/AdImage';
import SidebarAdStack from '@/components/SidebarAdStack'; import SidebarAdStack from '@/components/SidebarAdStack';
import { ADS_728X90, shuffle } from '@/lib/ads'; import { pickAds } from '@/lib/ads';
import { createClient } from '@/lib/supabase/client'; import { createClient } from '@/lib/supabase/client';
interface ForumCategory { interface ForumCategory {
@@ -632,9 +632,9 @@ export default function ForumIndexPage() {
</div> </div>
{/* In-content 728x90 banner */} {/* In-content 728x90 banner */}
{ADS_728X90.length > 0 && ( {pickAds("728x90", 1).length > 0 && (
<div className="mt-10 hidden md:flex justify-center" aria-label="Advertisement"> <div className="mt-10 hidden md:flex justify-center" aria-label="Advertisement">
<AdImage ad={shuffle(ADS_728X90)[0]} page="forum" /> <AdImage ad={pickAds("728x90", 1)[0]} page="forum" />
</div> </div>
)} )}

View File

@@ -5,7 +5,7 @@ import { useParams } from 'next/navigation';
import Header from '@/components/Header'; import Header from '@/components/Header';
import Footer from '@/components/Footer'; import Footer from '@/components/Footer';
import AdImage from '@/components/AdImage'; import AdImage from '@/components/AdImage';
import { ADS_300X250, rotateAll } from '@/lib/ads'; import { pickAds, rotateAll } from '@/lib/ads';
import { createClient } from '@/lib/supabase/client'; import { createClient } from '@/lib/supabase/client';
import SidebarAdStack from "@/components/SidebarAdStack"; import SidebarAdStack from "@/components/SidebarAdStack";
import LinkifyPlainText from "@/components/LinkifyPlainText"; import LinkifyPlainText from "@/components/LinkifyPlainText";
@@ -13,7 +13,7 @@ import CompanyMentionsHover from "@/components/CompanyMentionsHover";
// Inline ad slot inserted between forum replies every N posts. // Inline ad slot inserted between forum replies every N posts.
function InThreadAd({ adIndex }: { adIndex: number }) { function InThreadAd({ adIndex }: { adIndex: number }) {
const pool = ADS_300X250; const pool = rotateAll("300x250");
if (pool.length === 0) return null; if (pool.length === 0) return null;
const ad = pool[adIndex % pool.length]; const ad = pool[adIndex % pool.length];
return ( return (

View File

@@ -5,7 +5,7 @@ import Header from "@/components/Header";
import Footer from "@/components/Footer"; import Footer from "@/components/Footer";
import AppImage from "@/components/ui/AppImage"; import AppImage from "@/components/ui/AppImage";
import AdImage from "@/components/AdImage"; import AdImage from "@/components/AdImage";
import { ADS_728X90, shuffle } from "@/lib/ads"; import { pickAds } from "@/lib/ads";
import { getLegacyArticlesBySection } from "@/lib/articles/legacy-source"; import { getLegacyArticlesBySection } from "@/lib/articles/legacy-source";
import SidebarAdStack from "@/components/SidebarAdStack"; import SidebarAdStack from "@/components/SidebarAdStack";
@@ -91,9 +91,9 @@ export default async function GearPage() {
</div> </div>
{/* In-content 728x90 banner */} {/* In-content 728x90 banner */}
{ADS_728X90.length > 0 && ( {pickAds("728x90", 1).length > 0 && (
<div className="mt-12 hidden md:flex justify-center" aria-label="Advertisement"> <div className="mt-12 hidden md:flex justify-center" aria-label="Advertisement">
<AdImage ad={shuffle(ADS_728X90)[0]} page="gear" /> <AdImage ad={pickAds("728x90", 1)[0]} page="gear" />
</div> </div>
)} )}
</main> </main>

View File

@@ -5,7 +5,7 @@ import AppImage from "@/components/ui/AppImage";
import Header from "@/components/Header"; import Header from "@/components/Header";
import Footer from "@/components/Footer"; import Footer from "@/components/Footer";
import AdImage from "@/components/AdImage"; import AdImage from "@/components/AdImage";
import { ADS_728X90, shuffle } from "@/lib/ads"; import { pickAds } from "@/lib/ads";
import { createClient } from "@/lib/supabase/client"; import { createClient } from "@/lib/supabase/client";
import SidebarAdStack from "@/components/SidebarAdStack"; import SidebarAdStack from "@/components/SidebarAdStack";
@@ -315,9 +315,9 @@ export default function ReadingListPage() {
</div> </div>
{/* In-content 728x90 banner */} {/* In-content 728x90 banner */}
{ADS_728X90.length > 0 && ( {pickAds("728x90", 1).length > 0 && (
<div className="max-w-container mx-auto px-4 mt-10 mb-6 hidden md:flex justify-center" aria-label="Advertisement"> <div className="max-w-container mx-auto px-4 mt-10 mb-6 hidden md:flex justify-center" aria-label="Advertisement">
<AdImage ad={shuffle(ADS_728X90)[0]} page="reading-list" /> <AdImage ad={pickAds("728x90", 1)[0]} page="reading-list" />
</div> </div>
)} )}
</main> </main>

View File

@@ -4,7 +4,7 @@ import Header from "@/components/Header";
import Footer from "@/components/Footer"; import Footer from "@/components/Footer";
import AppImage from "@/components/ui/AppImage"; import AppImage from "@/components/ui/AppImage";
import AdImage from "@/components/AdImage"; import AdImage from "@/components/AdImage";
import { ADS_728X90, shuffle } from "@/lib/ads"; import { pickAds } from "@/lib/ads";
import { searchLegacyArticles } from "@/lib/articles/legacy-source"; import { searchLegacyArticles } from "@/lib/articles/legacy-source";
import SidebarAdStack from "@/components/SidebarAdStack"; import SidebarAdStack from "@/components/SidebarAdStack";
@@ -99,9 +99,9 @@ export default async function SearchPage({ searchParams }: SearchPageProps) {
</div> </div>
{/* In-content 728x90 banner */} {/* In-content 728x90 banner */}
{ADS_728X90.length > 0 && ( {pickAds("728x90", 1).length > 0 && (
<div className="mt-10 hidden md:flex justify-center" aria-label="Advertisement"> <div className="mt-10 hidden md:flex justify-center" aria-label="Advertisement">
<AdImage ad={shuffle(ADS_728X90)[0]} page="search" /> <AdImage ad={pickAds("728x90", 1)[0]} page="search" />
</div> </div>
)} )}
</main> </main>

View File

@@ -5,7 +5,7 @@ import Header from "@/components/Header";
import Footer from "@/components/Footer"; import Footer from "@/components/Footer";
import AdImage from "@/components/AdImage"; import AdImage from "@/components/AdImage";
import SidebarAdStack from "@/components/SidebarAdStack"; import SidebarAdStack from "@/components/SidebarAdStack";
import { ADS_728X90, shuffle } from "@/lib/ads"; import { pickAds } from "@/lib/ads";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export const revalidate = 600; export const revalidate = 600;
@@ -215,9 +215,9 @@ export default async function EventCoveragePage({
</header> </header>
{/* In-content 728x90 banner */} {/* In-content 728x90 banner */}
{ADS_728X90.length > 0 && ( {pickAds("728x90", 1).length > 0 && (
<div className="my-8 hidden md:flex justify-center" aria-label="Advertisement"> <div className="my-8 hidden md:flex justify-center" aria-label="Advertisement">
<AdImage ad={shuffle(ADS_728X90)[0]} page="show-coverage" /> <AdImage ad={pickAds("728x90", 1)[0]} page="show-coverage" />
</div> </div>
)} )}

View File

@@ -3,7 +3,7 @@ import { createClient } from "@supabase/supabase-js";
import Header from "@/components/Header"; import Header from "@/components/Header";
import Footer from "@/components/Footer"; import Footer from "@/components/Footer";
import AdImage from "@/components/AdImage"; import AdImage from "@/components/AdImage";
import { ADS_728X90, shuffle } from "@/lib/ads"; import { pickAds } from "@/lib/ads";
import SidebarAdStack from "@/components/SidebarAdStack"; import SidebarAdStack from "@/components/SidebarAdStack";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -94,9 +94,9 @@ export default async function ShowCoverageIndex() {
</section> </section>
{/* In-content 728x90 banner */} {/* In-content 728x90 banner */}
{ADS_728X90.length > 0 && ( {pickAds("728x90", 1).length > 0 && (
<div className="my-10 hidden md:flex justify-center" aria-label="Advertisement"> <div className="my-10 hidden md:flex justify-center" aria-label="Advertisement">
<AdImage ad={shuffle(ADS_728X90)[0]} page="show-coverage" /> <AdImage ad={pickAds("728x90", 1)[0]} page="show-coverage" />
</div> </div>
)} )}

View File

@@ -5,7 +5,7 @@ import Header from "@/components/Header";
import Footer from "@/components/Footer"; import Footer from "@/components/Footer";
import AppImage from "@/components/ui/AppImage"; import AppImage from "@/components/ui/AppImage";
import AdImage from "@/components/AdImage"; import AdImage from "@/components/AdImage";
import { ADS_728X90, shuffle } from "@/lib/ads"; import { pickAds } from "@/lib/ads";
import { getLegacyArticlesBySection } from "@/lib/articles/legacy-source"; import { getLegacyArticlesBySection } from "@/lib/articles/legacy-source";
import SidebarAdStack from "@/components/SidebarAdStack"; import SidebarAdStack from "@/components/SidebarAdStack";
@@ -213,9 +213,9 @@ export default async function TechnologyPage({ searchParams }: PageProps) {
</div> </div>
{/* In-content 728x90 banner */} {/* In-content 728x90 banner */}
{ADS_728X90.length > 0 && ( {pickAds("728x90", 1).length > 0 && (
<div className="mt-12 hidden md:flex justify-center" aria-label="Advertisement"> <div className="mt-12 hidden md:flex justify-center" aria-label="Advertisement">
<AdImage ad={shuffle(ADS_728X90)[0]} page="technology" /> <AdImage ad={pickAds("728x90", 1)[0]} page="technology" />
</div> </div>
)} )}
</main> </main>

View File

@@ -8,7 +8,7 @@ import {
FacebookIcon } from FacebookIcon } from
"@/components/ui/Icons"; "@/components/ui/Icons";
import AdImage from "@/components/AdImage"; import AdImage from "@/components/AdImage";
import { ADS_728X90, shuffle } from "@/lib/ads"; import { pickAds } from "@/lib/ads";
// DO NOT OVERRIDE — Footer column structure and link targets // DO NOT OVERRIDE — Footer column structure and link targets
const footerColumns = [ const footerColumns = [
@@ -65,9 +65,9 @@ export default function Footer() {
<footer className="bg-[#0d0d0d] border-t border-[#222]"> <footer className="bg-[#0d0d0d] border-t border-[#222]">
{/* Footer Ad Row — site-wide 728x90 (rotates if multiple in pool) */} {/* Footer Ad Row — site-wide 728x90 (rotates if multiple in pool) */}
<div className="max-w-container mx-auto px-4 pt-8 pb-4"> <div className="max-w-container mx-auto px-4 pt-8 pb-4">
{ADS_728X90.length > 0 && ( {pickAds("728x90", 1).length > 0 && (
<div className="flex justify-center mb-8"> <div className="flex justify-center mb-8">
<AdImage ad={shuffle(ADS_728X90)[0]} page="footer" /> <AdImage ad={pickAds("728x90", 1)[0]} page="footer" />
</div> </div>
)} )}

View File

@@ -19,7 +19,7 @@ import AdImage from "@/components/AdImage";
import EventsDropdown from "@/components/EventsDropdown"; import EventsDropdown from "@/components/EventsDropdown";
import AboutDropdown from "@/components/AboutDropdown"; import AboutDropdown from "@/components/AboutDropdown";
import DualSpeedTicker from "@/components/DualSpeedTicker"; import DualSpeedTicker from "@/components/DualSpeedTicker";
import { ADS_728X90, shuffle } from "@/lib/ads"; import { pickAds } from "@/lib/ads";
interface NavItem { interface NavItem {
label: string; label: string;
@@ -273,9 +273,9 @@ export default function Header() {
</Link> </Link>
{/* Header 728x90 banner — centered between logo and right side */} {/* Header 728x90 banner — centered between logo and right side */}
{ADS_728X90.length > 0 && ( {pickAds("728x90", 1).length > 0 && (
<div className="hidden md:flex flex-1 justify-center min-w-0 overflow-hidden"> <div className="hidden md:flex flex-1 justify-center min-w-0 overflow-hidden">
<AdImage ad={shuffle(ADS_728X90)[0]} priority page="header" /> <AdImage ad={pickAds("728x90", 1)[0]} priority page="header" />
</div> </div>
)} )}

View File

@@ -1,11 +1,14 @@
"use client"; "use client";
import { useMemo } from "react"; import { useMemo } from "react";
import AdImage from "./AdImage"; import AdImage from "./AdImage";
import { FIXED_300X600, rotateAll, type Ad } from "@/lib/ads"; import { pickAds, rotateAll, type Ad } from "@/lib/ads";
// Renders the fixed 300x600 at the top (no rotation) followed by EVERY // Renders the 300x600 at the top followed by EVERY 300x250 banner stacked
// 300x250 banner stacked vertically in a shuffled order. Adding more // vertically in a shuffled order. Both reads happen at render time so the
// 300x250 entries to ads.ts automatically grows this stack. // centralized RMP cache (which populates async after module import) is
// picked up — using the module-level FIXED_300X600 constant captured the
// FALLBACK ad with its direct bmd.link click_url and bypassed the click
// logger entirely.
export default function SidebarAdStack({ export default function SidebarAdStack({
excludeSrcs = [], excludeSrcs = [],
className = "", className = "",
@@ -13,6 +16,10 @@ export default function SidebarAdStack({
excludeSrcs?: string[]; excludeSrcs?: string[];
className?: string; className?: string;
}) { }) {
const top600 = useMemo<Ad | null>(
() => pickAds("300x600", 1)[0] ?? null,
[excludeSrcs.join("|")],
);
const rotating = useMemo<Ad[]>( const rotating = useMemo<Ad[]>(
() => rotateAll("300x250", new Set(excludeSrcs)), () => rotateAll("300x250", new Set(excludeSrcs)),
[excludeSrcs.join("|")], [excludeSrcs.join("|")],
@@ -20,7 +27,7 @@ export default function SidebarAdStack({
return ( return (
<div className={`flex flex-col items-end gap-4 ${className}`}> <div className={`flex flex-col items-end gap-4 ${className}`}>
{FIXED_300X600 && <AdImage ad={FIXED_300X600} priority />} {top600 && <AdImage ad={top600} priority />}
{rotating.map((ad) => ( {rotating.map((ad) => (
<AdImage key={ad.src ?? ad.label} ad={ad} /> <AdImage key={ad.src ?? ad.label} ad={ad} />
))} ))}

View File

@@ -99,15 +99,18 @@ async function fetchPlacement(size: Ad["size"]): Promise<Ad[]> {
if (!res.ok) return []; if (!res.ok) return [];
const data = await res.json() as { banners?: RmpBanner[] }; const data = await res.json() as { banners?: RmpBanner[] };
return (data.banners || []).map((b): Ad => ({ return (data.banners || []).map((b): Ad => ({
// Prefer the human-readable slug for the centralized click URL; fall back
// to the campaign UUID if the slug column ever ends up empty.
slug: b.slug || b.campaign_id, slug: b.slug || b.campaign_id,
campaign_id: b.campaign_id, campaign_id: b.campaign_id,
label: b.name, label: b.name,
size: b.placement, size: b.placement,
src: b.image_url, src: b.image_url,
alt: b.name, alt: b.name,
click_url: `https://www.relevantmediaproperties.com/${PROPERTY}/${b.slug || b.campaign_id}`, // All clicks route through advertising.RMP.com/r/<campaign_id> so they
// land in bb.ad_clicks — the single table the RMP analytics dashboard
// reads. Do not change to a different host or path; the legacy
// www.RMP.com/<property>/<slug> path writes to adv.banner_clicks which
// the dashboard does not query.
click_url: `${RMP_API}/r/${b.campaign_id}`,
})); }));
} catch { } catch {
return []; return [];
@@ -207,11 +210,8 @@ export function pickAds(
return all.slice(0, Math.min(n, all.length)); return all.slice(0, Math.min(n, all.length));
} }
// Sync exports for callers that still want the bare arrays. These also // Prime the cache once at module load. Callers must use rotateAll() or
// read the cache (so live edits surface) but fall back to FALLBACK. // pickAds() so the live cache (which fills async after import) is read at
export const ADS_728X90: Ad[] = (() => readCachedAds().filter((a) => a.size === "728x90"))(); // render time — the previous module-level IIFE constants captured the
export const ADS_300X250: Ad[] = (() => readCachedAds().filter((a) => a.size === "300x250"))(); // FALLBACK array forever and silently bypassed the centralized click logger.
export const FIXED_300X600: Ad | null = (() => readCachedAds().find((a) => a.size === "300x600") || null)();
// Prime the cache once at module load.
triggerRefresh(); triggerRefresh();