import { NextResponse, type NextRequest } from "next/server"; // PR firms / writers authenticate at the distribute platform, not here. // Legacy WordPress URLs still bounce there. /login is now a real BB login // page so the admin backend can use its own session (the BB Supabase cookie // doesn't transfer from distribute's domain). const AUTH_TARGET = "https://distribution.relevantmediaproperties.com/login"; const REDIRECT_PATHS = new Set([ "/wp-login", "/wp-login.php", "/wp-admin", "/client-login", ]); // ─── Server-side impression beacon (AdSanity-equivalent) ──────────────────── // // Why: client-side beacons drop 20-50% to ad blockers, JS-disabled clients, // and lost beacons on fast navigation. AdSanity counted server-side — every // WP page render = 1 impression per zone. To get parity with the historical // numbers our advertisers expect, we fire impressions here on every HTML // page request. // // For each slot (728x90, 300x600, 300x250) we pick ONE banner from the // rotation pool per page render — same model AdSanity used. The rotation // pool is fetched from advertising-rmp and cached per-edge-worker for 5min. const RMP_BASE = (process.env.NEXT_PUBLIC_RMP_ADS_API || "https://advertising.relevantmediaproperties.com").replace(/\/$/, ""); const PROPERTY = "avbeat"; const SLOTS = ["728x90", "300x600", "300x250"] as const; type Slot = typeof SLOTS[number]; // Edge worker-local cache. Each region's edge will refresh independently. let poolCache: Record = { "728x90": [], "300x600": [], "300x250": [] }; let poolLoadedAt = 0; const POOL_TTL_MS = 5 * 60 * 1000; async function loadPool() { if (Date.now() - poolLoadedAt < POOL_TTL_MS) return; try { const results = await Promise.all( SLOTS.map(async (size) => { const res = await fetch(`${RMP_BASE}/api/banners/${PROPERTY}/${size}`, { cache: "no-store", }); if (!res.ok) return [size, [] as string[]] as const; const d = (await res.json()) as { banners?: Array<{ campaign_id?: string }> }; return [size, (d.banners || []).map((b) => b.campaign_id).filter(Boolean) as string[]] as const; }) ); const next: Record = { "728x90": [], "300x600": [], "300x250": [] }; for (const [size, ids] of results) next[size] = ids; poolCache = next; poolLoadedAt = Date.now(); } catch { // keep stale cache on failure } } // Paths that don't count as a "page view" — assets, APIs, edge probes. const SKIP_RE = /^\/(api|_next|legacy|favicon|robots\.txt|sitemap|opensearch|.*\.(?:css|js|map|png|jpg|jpeg|gif|webp|svg|ico|woff|woff2|ttf|eot|mp4|webm|xml|txt|json))$/i; function fireImpression(req: NextRequest, campaign_id: string) { const url = new URL(req.url); fetch(`${RMP_BASE}/api/track/impression`, { method: "POST", headers: { "content-type": "application/json", "user-agent": req.headers.get("user-agent") || "", "x-forwarded-for": req.headers.get("x-forwarded-for") || req.headers.get("cf-connecting-ip") || "", "referer": req.headers.get("referer") || "", }, body: JSON.stringify({ campaign_id, page_path: url.pathname, page_url: req.url, viewport: false, }), cache: "no-store", keepalive: true, }).catch(() => undefined); } export async function middleware(req: NextRequest) { const path = req.nextUrl.pathname; // 1) auth redirects (legacy WP-style logins) if ( REDIRECT_PATHS.has(path) || path.startsWith("/wp-admin/") || path.startsWith("/wp-login/") ) { return NextResponse.redirect(AUTH_TARGET, 301); } // 2) server-side impression beacons per page render if (!SKIP_RE.test(path)) { await loadPool(); for (const slot of SLOTS) { const pool = poolCache[slot]; if (pool.length === 0) continue; const picked = pool[Math.floor(Math.random() * pool.length)]; fireImpression(req, picked); } } return NextResponse.next(); } // Match every path (impression beacon needs site-wide coverage) — the // SKIP_RE inside the handler filters out asset/API requests so we only // count real page views. export const config = { matcher: [ "/((?!api/|_next/|legacy/|.*\\.).*)", "/wp-login", "/wp-login.php", "/wp-admin/:path*", "/wp-login/:path*", "/client-login", ], };