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

@@ -99,15 +99,18 @@ async function fetchPlacement(size: Ad["size"]): Promise<Ad[]> {
if (!res.ok) return [];
const data = await res.json() as { banners?: RmpBanner[] };
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,
campaign_id: b.campaign_id,
label: b.name,
size: b.placement,
src: b.image_url,
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 {
return [];
@@ -207,11 +210,8 @@ export function pickAds(
return all.slice(0, Math.min(n, all.length));
}
// Sync exports for callers that still want the bare arrays. These also
// read the cache (so live edits surface) but fall back to FALLBACK.
export const ADS_728X90: Ad[] = (() => readCachedAds().filter((a) => a.size === "728x90"))();
export const ADS_300X250: Ad[] = (() => readCachedAds().filter((a) => a.size === "300x250"))();
export const FIXED_300X600: Ad | null = (() => readCachedAds().find((a) => a.size === "300x600") || null)();
// Prime the cache once at module load.
// Prime the cache once at module load. Callers must use rotateAll() or
// pickAds() so the live cache (which fills async after import) is read at
// render time — the previous module-level IIFE constants captured the
// FALLBACK array forever and silently bypassed the centralized click logger.
triggerRefresh();