ads: switch to centralized RMP banner source; impression beacon → advertising.rmp; rearm viewport observer (12s debounce) for re-entry counting

This commit is contained in:
Ryan Salazar
2026-05-22 05:51:54 +00:00
parent e5b09fe6d7
commit 04efa3933a
2 changed files with 108 additions and 39 deletions

View File

@@ -13,6 +13,7 @@ import { createClient } from "@supabase/supabase-js";
export interface Ad {
slug?: string;
campaign_id?: string;
label: string;
size: "300x250" | "300x600" | "728x90";
src: string;
@@ -70,7 +71,65 @@ interface DbRow {
size: "300x250" | "300x600" | "728x90";
}
// Centralized banner source: advertising.relevantmediaproperties.com
// serves /api/banners/<property>/<placement> with CORS + 5min cache.
// We fetch all three sizes once and merge.
const RMP_API = process.env.NEXT_PUBLIC_RMP_ADS_API
|| "https://advertising.relevantmediaproperties.com";
const PROPERTY = process.env.NEXT_PUBLIC_RMP_PROPERTY || "broadcastbeat";
interface RmpBanner {
campaign_id: string;
name: string;
image_url: string;
image_webp_url?: string | null;
click_url: string;
width: number;
height: number;
placement: "300x250" | "300x600" | "728x90";
property: string;
slug?: string;
}
async function fetchPlacement(size: Ad["size"]): Promise<Ad[]> {
try {
const res = await fetch(`${RMP_API}/api/banners/${PROPERTY}/${size}`, {
next: { revalidate: 300 },
});
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}`,
}));
} catch {
return [];
}
}
async function loadFromDb(): Promise<Ad[]> {
// 1) Try centralized RMP source first (preferred path).
try {
const [a, b, c] = await Promise.all([
fetchPlacement("300x250"),
fetchPlacement("300x600"),
fetchPlacement("728x90"),
]);
const all = [...a, ...b, ...c];
if (all.length > 0) return all;
} catch {
/* fall through */
}
// 2) Legacy fallback: query BB's own bb.banner_creatives (kept until the
// centralized RMP source is fully verified in prod).
if (!SUPABASE_URL || !SUPABASE_ANON) return FALLBACK;
try {
const sb = createClient(SUPABASE_URL, SUPABASE_ANON, {
@@ -84,12 +143,7 @@ async function loadFromDb(): Promise<Ad[]> {
.eq("status", "active")
.or(`start_date.is.null,start_date.lte.${nowIso}`)
.or(`end_date.is.null,end_date.gte.${nowIso}`);
if (error || !data || data.length === 0) {
if (typeof console !== "undefined") {
console.warn("[ads] db query empty/error, using fallback:", error?.message || "no rows");
}
return FALLBACK;
}
if (error || !data || data.length === 0) return FALLBACK;
return (data as DbRow[]).map((r) => ({
slug: r.slug,
label: r.name,
@@ -98,10 +152,7 @@ async function loadFromDb(): Promise<Ad[]> {
alt: r.name,
click_url: r.click_url || undefined,
}));
} catch (err: any) {
if (typeof console !== "undefined") {
console.warn("[ads] db query threw, using fallback:", err?.message);
}
} catch {
return FALLBACK;
}
}