From 8719b628085912deda55b03b894587182c6e776e Mon Sep 17 00:00:00 2001 From: RMP Ops Date: Thu, 18 Jun 2026 03:26:26 +0000 Subject: [PATCH] ads: adopt canonical RMP BannerTracking (AdSanity-style gross + viewport dedup) --- src/app/layout.tsx | 2 + src/components/BannerTracking.tsx | 168 ++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 src/components/BannerTracking.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 27745e6..b5ed3f3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -36,6 +36,7 @@ import GoogleAnalytics from '@/components/GoogleAnalytics'; import CookieConsent from '@/components/CookieConsent'; import NewsletterModal from '@/components/NewsletterModal'; +import BannerTracking from '@/components/BannerTracking'; export const viewport: Viewport = { width: 'device-width', initialScale: 1 @@ -150,6 +151,7 @@ export default function RootLayout({ }} /> + {/* Skip to main content — WCAG 2.4.1 */} { + if (typeof window === "undefined") return; + + let sid = sessionStorage.getItem("rmp.session"); + if (!sid) { + sid = + typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" + ? crypto.randomUUID() + : Math.random().toString(36).slice(2) + Date.now().toString(36); + sessionStorage.setItem("rmp.session", sid); + } + + const fired = new WeakSet(); + const visibleByKey = new Map(); + const hiddenByUs = new WeakSet(); + + function isNested(el: Element): boolean { + return !!el.parentElement?.closest(SELECTOR); + } + + function fireBeacon(el: Element): void { + if (fired.has(el)) return; + if (isNested(el)) return; + fired.add(el); + + const campaign_id = el.getAttribute("data-campaign-id") || undefined; + const slug = el.getAttribute("data-ad-slug") || undefined; + if (!campaign_id && !slug) return; + + const payload = JSON.stringify({ + campaign_id, + slug, + property, + page_path: location.pathname, + page_url: location.href, + session_id: sid, + viewport: false, + }); + + try { + if (typeof navigator.sendBeacon === "function") { + navigator.sendBeacon( + ENDPOINT, + new Blob([payload], { type: "application/json" }) + ); + } else { + void fetch(ENDPOINT, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: payload, + keepalive: true, + }).catch(() => {}); + } + } catch { + // never throw to the page + } + } + + function keyOf(el: Element): string | null { + const cid = el.getAttribute("data-campaign-id"); + if (cid) return `c:${cid}`; + + const anchor = + el.matches("a[href]") + ? el + : el.querySelector("a[href]") ?? el.closest("a[href]"); + const href = anchor?.getAttribute("href"); + if (href && !href.startsWith("#")) return `h:${href}`; + + const img = el.querySelector("img[src]") as HTMLImageElement | null; + const src = img?.getAttribute("src"); + if (src) return `i:${src}`; + + return null; + } + + const io = new IntersectionObserver( + (entries) => { + for (const e of entries) { + const el = e.target; + const key = keyOf(el); + if (!key) continue; + + if (e.isIntersecting) { + const cur = visibleByKey.get(key); + if (cur && cur !== el) { + (el as HTMLElement).style.setProperty("display", "none", "important"); + el.setAttribute("data-deduped", "auto"); + hiddenByUs.add(el); + } else { + visibleByKey.set(key, el); + } + } else { + if (hiddenByUs.has(el)) continue; + if (visibleByKey.get(key) === el) { + visibleByKey.delete(key); + const candidates = Array.from( + document.querySelectorAll(SELECTOR) + ).filter( + (c) => + !isNested(c) && keyOf(c) === key && hiddenByUs.has(c) + ); + if (candidates.length > 0) { + const c = candidates[0] as HTMLElement; + c.style.removeProperty("display"); + c.removeAttribute("data-deduped"); + hiddenByUs.delete(c); + } + } + } + } + }, + { threshold: 0.3 } + ); + + function register(el: Element): void { + if (isNested(el)) return; + fireBeacon(el); + io.observe(el); + } + + document.querySelectorAll(SELECTOR).forEach(register); + + const mo = new MutationObserver((muts) => { + for (const m of muts) { + m.addedNodes.forEach((n) => { + if (n.nodeType !== 1) return; + const el = n as Element; + if (el.matches?.(SELECTOR)) register(el); + el.querySelectorAll?.(SELECTOR).forEach(register); + }); + } + }); + mo.observe(document.body, { childList: true, subtree: true }); + + return () => { + io.disconnect(); + mo.disconnect(); + }; + }, [property]); + + return null; +}