diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b5ed3f3..3463d63 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -37,6 +37,7 @@ import CookieConsent from '@/components/CookieConsent'; import NewsletterModal from '@/components/NewsletterModal'; import BannerTracking from '@/components/BannerTracking'; +import SiteAnalyticsBeacon from '@/components/SiteAnalyticsBeacon'; export const viewport: Viewport = { width: 'device-width', initialScale: 1 @@ -152,6 +153,7 @@ export default function RootLayout({ + {/* Skip to main content — WCAG 2.4.1 */} /, which logs the click -// to adv.banner_clicks then 302s to the advertiser URL. No client-side -// click logging is needed. - const RMP_TRACK_URL = - (process.env.NEXT_PUBLIC_RMP_ADS_API || "https://advertising.relevantmediaproperties.com") - + "/api/track/impression"; + (process.env.NEXT_PUBLIC_RMP_ADS_API || "https://advertising.relevantmediaproperties.com") + + "/api/track/impression"; export default function AdImage({ ad, @@ -46,33 +28,31 @@ export default function AdImage({ }) { const containerRef = useRef(null); const beaconMount = useRef(false); - const lastViewportFire = useRef(0); useEffect(() => { if (!ad.src) return; const id = ad.campaign_id || ad.slug; if (!id) return; - // One impression per pageview per banner, fired on mount. No viewport - // gate — too many ad slots sit below-the-fold and the IO threshold - // suppressed real signal. if (!beaconMount.current) { beaconMount.current = true; - sendImpression(id, page, false); + sendImpression(id, ad.slug, page, false); } }, [ad.src, ad.slug, ad.campaign_id, page]); if (!ad.src) return null; const [w, h] = ad.size.split("x").map((n) => parseInt(n, 10)); - // ad.click_url comes from lib/ads.ts already pointing at the centralized - // redirect (https://www.relevantmediaproperties.com//). - // Legacy fallback rows still have a direct click_url; either way we send - // the user there. - const clickHref = ad.click_url || (ad.slug ? `/r/${ad.slug}` : ""); + const clickHref = ad.click_url || (ad.campaign_id ? `/r/${ad.campaign_id}` : ad.slug ? `/r/${ad.slug}` : ""); return ( -
+
{clickHref ? ( @@ -84,12 +64,16 @@ export default function AdImage({ ); } -function sendImpression(id: string, page: string | undefined, viewport: boolean) { +function sendImpression( + id: string, + slug: string | undefined, + page: string | undefined, + viewport: boolean, +) { if (typeof window === "undefined") return; - // The RMP track endpoint accepts campaign_id (UUID). If we only have a - // slug, the centralized endpoint resolves it to a UUID server-side. const payload = JSON.stringify({ campaign_id: id, + slug: slug || undefined, page_path: page || window.location.pathname, page_url: window.location.href, viewport, @@ -97,19 +81,16 @@ function sendImpression(id: string, page: string | undefined, viewport: boolean) try { const ok = typeof navigator.sendBeacon === "function" && - navigator.sendBeacon( - RMP_TRACK_URL, - new Blob([payload], { type: "application/json" }), - ); + navigator.sendBeacon(RMP_TRACK_URL, new Blob([payload], { type: "application/json" })); if (ok) return; } catch { - // fall through to fetch + /* fall through */ } fetch(RMP_TRACK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: payload, keepalive: true, - mode: "no-cors", + mode: "cors", }).catch(() => undefined); } diff --git a/src/components/Footer.tsx.bak-jtr-footer-20260717 b/src/components/Footer.tsx.bak-jtr-footer-20260717 new file mode 100644 index 0000000..e9f1564 --- /dev/null +++ b/src/components/Footer.tsx.bak-jtr-footer-20260717 @@ -0,0 +1,147 @@ +import React from "react"; +import Link from "next/link"; +import AppImage from "@/components/ui/AppImage"; +import AnimatedLogo from "@/components/AnimatedLogo"; +import { + LinkedInIcon, + InstagramIcon, + TwitterXIcon, + FacebookIcon } from +"@/components/ui/Icons"; +import AdImage from "@/components/AdImage"; +import { pickAds } from "@/lib/ads"; + +// DO NOT OVERRIDE — Footer column structure and link targets +const footerColumns = [ +{ + heading: "About", + links: [ + { label: "About AV Beat", href: "/about" }, + { label: "Our Mission", href: "/about#mission" }, + { label: "Editorial Team", href: "/about#team" }, + { label: "Advertise With Us", href: "/advertise" }, + { label: "Contact Us", href: "/advertise#contact" }, + { label: "Newsletter", href: "/home-page#newsletter" }, + { label: "Video Archive", href: "/video-archive" }] + +}, +{ + heading: "Categories", + links: [ + { label: "Industry News", href: "/news" }, + { label: "Gear & Reviews", href: "/gear" }, + { label: "Show Coverage", href: "/show-coverage" }, + { label: "Production Technology", href: "/technology" }, + { label: "Live Production", href: "/news?category=live-production" }, + { label: "AI & Automation", href: "/technology?category=ai" }, + { label: "Streaming", href: "/technology?category=streaming" }, + { label: "Audio", href: "/gear?category=audio" }] + +}, +{ + heading: "Events", + links: [ + { label: "NAB Show", href: "/show-coverage" }, + { label: "IBC", href: "/show-coverage?event=ibc" }, + { label: "Cine Gear", href: "/show-coverage?event=cine-gear" }, + { label: "SXSW", href: "/show-coverage?event=sxsw" }, + { label: "Streaming Summit", href: "/show-coverage?event=streaming-summit" }, + { label: "All Events", href: "/show-coverage" }] + +}, +{ + heading: "Follow Us", + links: [ + { label: "LinkedIn", href: "https://linkedin.com/company/avbeat" }, + { label: "Twitter / X", href: "https://twitter.com/avbeat" }, + { label: "Facebook", href: "https://facebook.com/avbeat" }, + { label: "Instagram", href: "https://instagram.com/avbeat" }, + { label: "YouTube", href: "https://youtube.com/@avbeat" }, + { label: "RSS Feed", href: "/rss" }] + +}]; + + +export default function Footer() { + return ( + ); + +} \ No newline at end of file diff --git a/src/components/SiteAnalyticsBeacon.tsx b/src/components/SiteAnalyticsBeacon.tsx new file mode 100644 index 0000000..ed7f5e4 --- /dev/null +++ b/src/components/SiteAnalyticsBeacon.tsx @@ -0,0 +1,105 @@ +'use client'; + +import { Suspense, useEffect, useRef } from 'react'; +import { usePathname, useSearchParams } from 'next/navigation'; + +const ENDPOINT = + (typeof process !== 'undefined' && process.env.NEXT_PUBLIC_RMP_ADS_API + ? process.env.NEXT_PUBLIC_RMP_ADS_API + : 'https://advertising.relevantmediaproperties.com') + '/api/analytics/pageview'; + +const ARTICLE_PREFIXES = ['/news/', '/articles/', '/article/', '/blog/']; +const SKIP_PREFIXES = ['/admin', '/api', '/login', '/client-login', '/wp-admin', '/marketplace/admin']; + +function getSessionId(): string { + try { + let sid = sessionStorage.getItem('rmp_analytics_sid'); + if (!sid) { + sid = + typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' + ? crypto.randomUUID() + : `as_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`; + sessionStorage.setItem('rmp_analytics_sid', sid); + } + return sid; + } catch { + return `as_${Date.now()}`; + } +} + +function utmParams(): Record { + if (typeof window === 'undefined') return {}; + const p = new URLSearchParams(window.location.search); + const out: Record = {}; + for (const k of ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']) { + out[k] = p.get(k); + } + return out; +} + +function fireView(propertySlug: string, pathname: string) { + if (typeof window === 'undefined') return; + if (SKIP_PREFIXES.some((p) => pathname.startsWith(p))) return; + + const isArticle = ARTICLE_PREFIXES.some((p) => pathname.startsWith(p)); + const articleSlug = isArticle ? pathname.split('/').filter(Boolean).pop() || null : null; + const utm = utmParams(); + + const payload = JSON.stringify({ + propertySlug, + pagePath: pathname, + pageUrl: window.location.href, + articleSlug, + referrer: document.referrer || null, + sessionId: getSessionId(), + eventType: 'view', + utmSource: utm.utm_source, + utmMedium: utm.utm_medium, + utmCampaign: utm.utm_campaign, + utmContent: utm.utm_content, + utmTerm: utm.utm_term, + }); + + try { + if (typeof navigator.sendBeacon === 'function') { + const ok = navigator.sendBeacon(ENDPOINT, new Blob([payload], { type: 'application/json' })); + if (ok) return; + } + } catch { + /* fall through */ + } + + fetch(ENDPOINT, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: payload, + keepalive: true, + mode: 'cors', + }).catch(() => undefined); +} + +function BeaconInner({ propertySlug }: { propertySlug: string }) { + const pathname = usePathname() || ''; + const searchParams = useSearchParams(); + const lastKey = useRef(''); + + useEffect(() => { + if (!propertySlug || !pathname) return; + const key = `${pathname}?${searchParams?.toString() || ''}`; + if (lastKey.current === key) return; + lastKey.current = key; + fireView(propertySlug, pathname); + }, [pathname, searchParams, propertySlug]); + + return null; +} + +/** Mount in root layout — tracks all public routes → advertising pageview API → adv.page_view_events. */ +export default function SiteAnalyticsBeacon({ propertySlug }: { propertySlug: string }) { + if (!propertySlug) return null; + return ( + + + + ); +} diff --git a/src/lib/ads.ts b/src/lib/ads.ts index af6cc75..44fa1fc 100644 --- a/src/lib/ads.ts +++ b/src/lib/ads.ts @@ -35,33 +35,39 @@ let CACHE: CacheRow | null = null; let REFRESH_IN_FLIGHT: Promise | null = null; const FALLBACK: Ad[] = [ - { slug: "liveu-728x90", label: "LiveU 728x90", size: "728x90", + { slug: "liveu-728x90", + campaign_id: "6031b0e3-2eee-4974-834f-c5c71b339aae", label: "LiveU 728x90", size: "728x90", src: "/legacy/ads/728-x-90-pxEN.gif", - alt: "LiveU 728x90", click_url: "https://bit.ly/4ghXVeq" }, + click_url: "https://advertising.relevantmediaproperties.com/r/6031b0e3-2eee-4974-834f-c5c71b339aae", { slug: "blackmagic-davinci-resolve-300x600", + campaign_id: "1d7aecbc-39ec-4e2f-a10f-b5394a9eaf27", label: "Blackmagic DaVinci Resolve 20", size: "300x600", src: "/legacy/ads/Davinci-Resolve-20_300x600.jpg", alt: "Blackmagic Design DaVinci Resolve 20", - click_url: "http://bmd.link/mRNsKu" }, - { slug: "magewell-pro-convert-300x250", label: "Magewell Pro-Convert", size: "300x250", + click_url: "https://advertising.relevantmediaproperties.com/r/1d7aecbc-39ec-4e2f-a10f-b5394a9eaf27", + { slug: "magewell-pro-convert-300x250", + campaign_id: "47f19e71-fc97-4aa5-998d-97e780b57729", label: "Magewell Pro-Convert", size: "300x250", src: "/legacy/ads/Magewell_Pro-Convert-IP-to-HDMI_Sports_NAB2026.gif", alt: "Magewell Pro-Convert IP to HDMI", - click_url: "https://www.magewell.com" }, + click_url: "https://advertising.relevantmediaproperties.com/r/47f19e71-fc97-4aa5-998d-97e780b57729", { slug: "liveu-payg-300x250", label: "LiveU PAYG", size: "300x250", src: "/legacy/ads/PAYG-300x250-1.gif", alt: "LiveU Pay-As-You-Go", click_url: "https://bit.ly/4ghXVeq" }, - { slug: "aja-colorbox-300x250", label: "AJA ColorBox", size: "300x250", + { slug: "aja-colorbox-300x250", + campaign_id: "75157edc-df21-460e-b08c-e0c8d8cfdf5b", label: "AJA ColorBox", size: "300x250", src: "/legacy/ads/aja_2025_colorbox_og_colorbox_300x250_en-2.gif", - alt: "AJA ColorBox", click_url: "https://www.aja.com" }, + click_url: "https://advertising.relevantmediaproperties.com/r/75157edc-df21-460e-b08c-e0c8d8cfdf5b", { slug: "zixi-300x250", label: "Zixi", size: "300x250", src: "/legacy/ads/Zixi_Ads_300x250.png", alt: "Zixi", click_url: "https://zixi.com/" }, - { slug: "telycam-300x250", label: "Telycam MixOne / ExploreXE", size: "300x250", + { slug: "telycam-300x250", + campaign_id: "d4fda1cd-1ffc-4ae5-bec3-1b38cf62a5e9", label: "Telycam MixOne / ExploreXE", size: "300x250", src: "/legacy/ads/Telycam_BroadcastBeat_300x250_MixOne-ExploreXE_with_NAB2026_2f30157f.gif", - alt: "Telycam MixOne / ExploreXE — 2026 NAB Show", click_url: "https://telycam.com/" }, - { slug: "lectrosonics-300x250", label: "Lectrosonics", size: "300x250", + click_url: "https://advertising.relevantmediaproperties.com/r/d4fda1cd-1ffc-4ae5-bec3-1b38cf62a5e9", + { slug: "lectrosonics-300x250", + campaign_id: "e69cb845-5526-4165-bca1-964104b70c6b", label: "Lectrosonics", size: "300x250", src: "/legacy/ads/300x250-banner-Our-Story-film_resize.jpg", - alt: "Lectrosonics Our Story", click_url: "https://www.lectrosonics.com/" }, + click_url: "https://advertising.relevantmediaproperties.com/r/e69cb845-5526-4165-bca1-964104b70c6b", ]; interface DbRow {