feat(avbeat): tracked FALLBACK /r/ + SiteAnalyticsBeacon (PIN 4769)
This commit is contained in:
@@ -37,6 +37,7 @@ import CookieConsent from '@/components/CookieConsent';
|
|||||||
import NewsletterModal from '@/components/NewsletterModal';
|
import NewsletterModal from '@/components/NewsletterModal';
|
||||||
|
|
||||||
import BannerTracking from '@/components/BannerTracking';
|
import BannerTracking from '@/components/BannerTracking';
|
||||||
|
import SiteAnalyticsBeacon from '@/components/SiteAnalyticsBeacon';
|
||||||
export const viewport: Viewport = {
|
export const viewport: Viewport = {
|
||||||
width: 'device-width',
|
width: 'device-width',
|
||||||
initialScale: 1
|
initialScale: 1
|
||||||
@@ -152,6 +153,7 @@ export default function RootLayout({
|
|||||||
</head>
|
</head>
|
||||||
<body className="bg-brand-bg text-brand-text">
|
<body className="bg-brand-bg text-brand-text">
|
||||||
<BannerTracking property="avbeat" />
|
<BannerTracking property="avbeat" />
|
||||||
|
<SiteAnalyticsBeacon propertySlug="avbeat" />
|
||||||
{/* Skip to main content — WCAG 2.4.1 */}
|
{/* Skip to main content — WCAG 2.4.1 */}
|
||||||
<a
|
<a
|
||||||
href="#main-content"
|
href="#main-content"
|
||||||
|
|||||||
@@ -13,27 +13,9 @@ interface Ad {
|
|||||||
click_url?: string;
|
click_url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Banner renderer with impression + click tracking.
|
|
||||||
//
|
|
||||||
// Impressions:
|
|
||||||
// - On mount: send 1 beacon (gross count).
|
|
||||||
// - On viewport entry (intersection ≥ 0.5): send another beacon — and STAY
|
|
||||||
// ARMED so re-entries count too (debounced to ≥ 12s between fires so
|
|
||||||
// scroll thrash doesn't multi-count).
|
|
||||||
//
|
|
||||||
// All impressions are POSTed to the centralized advertising endpoint
|
|
||||||
// at advertising.relevantmediaproperties.com — adv.campaigns.id is the
|
|
||||||
// canonical identifier on the wire (the slug is human-readable but the
|
|
||||||
// UUID is what RLS + analytics key off).
|
|
||||||
//
|
|
||||||
// Clicks: the Ad.click_url already points to the centralized redirect
|
|
||||||
// at relevantmediaproperties.com/<property>/<slug>, 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 =
|
const RMP_TRACK_URL =
|
||||||
(process.env.NEXT_PUBLIC_RMP_ADS_API || "https://advertising.relevantmediaproperties.com")
|
(process.env.NEXT_PUBLIC_RMP_ADS_API || "https://advertising.relevantmediaproperties.com") +
|
||||||
+ "/api/track/impression";
|
"/api/track/impression";
|
||||||
|
|
||||||
export default function AdImage({
|
export default function AdImage({
|
||||||
ad,
|
ad,
|
||||||
@@ -46,33 +28,31 @@ export default function AdImage({
|
|||||||
}) {
|
}) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const beaconMount = useRef(false);
|
const beaconMount = useRef(false);
|
||||||
const lastViewportFire = useRef(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ad.src) return;
|
if (!ad.src) return;
|
||||||
const id = ad.campaign_id || ad.slug;
|
const id = ad.campaign_id || ad.slug;
|
||||||
if (!id) return;
|
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) {
|
if (!beaconMount.current) {
|
||||||
beaconMount.current = true;
|
beaconMount.current = true;
|
||||||
sendImpression(id, page, false);
|
sendImpression(id, ad.slug, page, false);
|
||||||
}
|
}
|
||||||
}, [ad.src, ad.slug, ad.campaign_id, page]);
|
}, [ad.src, ad.slug, ad.campaign_id, page]);
|
||||||
|
|
||||||
if (!ad.src) return null;
|
if (!ad.src) return null;
|
||||||
|
|
||||||
const [w, h] = ad.size.split("x").map((n) => parseInt(n, 10));
|
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
|
const clickHref = ad.click_url || (ad.campaign_id ? `/r/${ad.campaign_id}` : ad.slug ? `/r/${ad.slug}` : "");
|
||||||
// redirect (https://www.relevantmediaproperties.com/<property>/<slug>).
|
|
||||||
// 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}` : "");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="ad-container">
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="ad-container"
|
||||||
|
data-campaign-id={ad.campaign_id || undefined}
|
||||||
|
data-ad-slug={ad.slug || undefined}
|
||||||
|
data-banner="1"
|
||||||
|
>
|
||||||
{clickHref ? (
|
{clickHref ? (
|
||||||
<a href={clickHref} target="_blank" rel="sponsored noopener noreferrer">
|
<a href={clickHref} target="_blank" rel="sponsored noopener noreferrer">
|
||||||
<AppImage src={ad.src} alt={ad.alt || ad.label} width={w} height={h} priority={priority} />
|
<AppImage src={ad.src} alt={ad.alt || ad.label} width={w} height={h} priority={priority} />
|
||||||
@@ -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;
|
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({
|
const payload = JSON.stringify({
|
||||||
campaign_id: id,
|
campaign_id: id,
|
||||||
|
slug: slug || undefined,
|
||||||
page_path: page || window.location.pathname,
|
page_path: page || window.location.pathname,
|
||||||
page_url: window.location.href,
|
page_url: window.location.href,
|
||||||
viewport,
|
viewport,
|
||||||
@@ -97,19 +81,16 @@ function sendImpression(id: string, page: string | undefined, viewport: boolean)
|
|||||||
try {
|
try {
|
||||||
const ok =
|
const ok =
|
||||||
typeof navigator.sendBeacon === "function" &&
|
typeof navigator.sendBeacon === "function" &&
|
||||||
navigator.sendBeacon(
|
navigator.sendBeacon(RMP_TRACK_URL, new Blob([payload], { type: "application/json" }));
|
||||||
RMP_TRACK_URL,
|
|
||||||
new Blob([payload], { type: "application/json" }),
|
|
||||||
);
|
|
||||||
if (ok) return;
|
if (ok) return;
|
||||||
} catch {
|
} catch {
|
||||||
// fall through to fetch
|
/* fall through */
|
||||||
}
|
}
|
||||||
fetch(RMP_TRACK_URL, {
|
fetch(RMP_TRACK_URL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: payload,
|
body: payload,
|
||||||
keepalive: true,
|
keepalive: true,
|
||||||
mode: "no-cors",
|
mode: "cors",
|
||||||
}).catch(() => undefined);
|
}).catch(() => undefined);
|
||||||
}
|
}
|
||||||
|
|||||||
147
src/components/Footer.tsx.bak-jtr-footer-20260717
Normal file
147
src/components/Footer.tsx.bak-jtr-footer-20260717
Normal file
@@ -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 (
|
||||||
|
<footer className="bg-[#F8FAFC] border-t border-[#DCE6F2]">
|
||||||
|
{/* Footer Ad Row — site-wide 728x90 (rotates if multiple in pool) */}
|
||||||
|
<div className="max-w-container mx-auto px-4 pt-8 pb-4">
|
||||||
|
{pickAds("728x90", 1).length > 0 && (
|
||||||
|
<div className="flex justify-center mb-8">
|
||||||
|
<AdImage ad={pickAds("728x90", 1)[0]} page="footer" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Multi-column link section */}
|
||||||
|
<div className="border-t border-[#DCE6F2] pt-8 pb-6 grid grid-cols-2 md:grid-cols-4 gap-8">
|
||||||
|
{footerColumns?.map((col) =>
|
||||||
|
<div key={col?.heading}>
|
||||||
|
<h4 className="font-body font-bold text-xs text-[#1D4ED8] uppercase tracking-widest mb-4 pb-2 border-b border-[#1D4ED8]/20">
|
||||||
|
{col?.heading}
|
||||||
|
</h4>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{col?.links?.map((link) =>
|
||||||
|
<li key={link?.label}>
|
||||||
|
{link?.href?.startsWith("http") ?
|
||||||
|
<a
|
||||||
|
href={link?.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="footer-link font-body text-sm text-[#475569] hover:text-[#1D4ED8] transition-colors leading-relaxed inline-block">
|
||||||
|
{link?.label}
|
||||||
|
</a> :
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={link?.href}
|
||||||
|
className="footer-link font-body text-sm text-[#475569] hover:text-[#1D4ED8] transition-colors leading-relaxed inline-block">
|
||||||
|
{link?.label}
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer Bottom */}
|
||||||
|
{/* DO NOT OVERRIDE — footer bottom bar layout */}
|
||||||
|
<div className="border-t border-[#DCE6F2] pt-5 pb-3 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
|
<Link
|
||||||
|
href="/home-page"
|
||||||
|
aria-label="AV Beat — home"
|
||||||
|
className="flex-shrink-0 opacity-90 hover:opacity-100 transition-opacity">
|
||||||
|
<AnimatedLogo size={36} variant="onLight" />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<a href="https://linkedin.com/company/avbeat" target="_blank" rel="noopener noreferrer" aria-label="AV Beat on LinkedIn"
|
||||||
|
className="text-[#475569] hover:text-[#1D4ED8] transition-colors social-icon">
|
||||||
|
<LinkedInIcon size={17} />
|
||||||
|
</a>
|
||||||
|
<a href="https://instagram.com/avbeat" target="_blank" rel="noopener noreferrer" aria-label="AV Beat on Instagram"
|
||||||
|
className="text-[#475569] hover:text-[#1D4ED8] transition-colors social-icon">
|
||||||
|
<InstagramIcon size={17} />
|
||||||
|
</a>
|
||||||
|
<a href="https://twitter.com/avbeat" target="_blank" rel="noopener noreferrer" aria-label="AV Beat on Twitter/X"
|
||||||
|
className="text-[#475569] hover:text-[#1D4ED8] transition-colors social-icon">
|
||||||
|
<TwitterXIcon size={17} />
|
||||||
|
</a>
|
||||||
|
<a href="https://facebook.com/avbeat" target="_blank" rel="noopener noreferrer" aria-label="AV Beat on Facebook"
|
||||||
|
className="text-[#475569] hover:text-[#1D4ED8] transition-colors social-icon">
|
||||||
|
<FacebookIcon size={17} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-xs font-body text-[#475569]">
|
||||||
|
<Link href="/privacy" className="hover:text-[#1D4ED8] transition-colors">Privacy Policy</Link>
|
||||||
|
<Link href="/terms" className="hover:text-[#1D4ED8] transition-colors">Terms of Service</Link>
|
||||||
|
<span className="inline-flex flex-col items-end gap-0.5 text-right"><span className="text-xs">© 2026 JTR Media Properties. All Rights Reserved.</span><span className="text-[8px] opacity-70 leading-none">Media representation by Relevant Media Properties</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</footer>);
|
||||||
|
|
||||||
|
}
|
||||||
105
src/components/SiteAnalyticsBeacon.tsx
Normal file
105
src/components/SiteAnalyticsBeacon.tsx
Normal file
@@ -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<string, string | null> {
|
||||||
|
if (typeof window === 'undefined') return {};
|
||||||
|
const p = new URLSearchParams(window.location.search);
|
||||||
|
const out: Record<string, string | null> = {};
|
||||||
|
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 (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<BeaconInner propertySlug={propertySlug} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -35,33 +35,39 @@ let CACHE: CacheRow | null = null;
|
|||||||
let REFRESH_IN_FLIGHT: Promise<void> | null = null;
|
let REFRESH_IN_FLIGHT: Promise<void> | null = null;
|
||||||
|
|
||||||
const FALLBACK: Ad[] = [
|
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",
|
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",
|
{ slug: "blackmagic-davinci-resolve-300x600",
|
||||||
|
campaign_id: "1d7aecbc-39ec-4e2f-a10f-b5394a9eaf27",
|
||||||
label: "Blackmagic DaVinci Resolve 20", size: "300x600",
|
label: "Blackmagic DaVinci Resolve 20", size: "300x600",
|
||||||
src: "/legacy/ads/Davinci-Resolve-20_300x600.jpg",
|
src: "/legacy/ads/Davinci-Resolve-20_300x600.jpg",
|
||||||
alt: "Blackmagic Design DaVinci Resolve 20",
|
alt: "Blackmagic Design DaVinci Resolve 20",
|
||||||
click_url: "http://bmd.link/mRNsKu" },
|
click_url: "https://advertising.relevantmediaproperties.com/r/1d7aecbc-39ec-4e2f-a10f-b5394a9eaf27",
|
||||||
{ slug: "magewell-pro-convert-300x250", label: "Magewell Pro-Convert", size: "300x250",
|
{ 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",
|
src: "/legacy/ads/Magewell_Pro-Convert-IP-to-HDMI_Sports_NAB2026.gif",
|
||||||
alt: "Magewell Pro-Convert IP to HDMI",
|
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",
|
{ slug: "liveu-payg-300x250", label: "LiveU PAYG", size: "300x250",
|
||||||
src: "/legacy/ads/PAYG-300x250-1.gif",
|
src: "/legacy/ads/PAYG-300x250-1.gif",
|
||||||
alt: "LiveU Pay-As-You-Go", click_url: "https://bit.ly/4ghXVeq" },
|
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",
|
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",
|
{ slug: "zixi-300x250", label: "Zixi", size: "300x250",
|
||||||
src: "/legacy/ads/Zixi_Ads_300x250.png",
|
src: "/legacy/ads/Zixi_Ads_300x250.png",
|
||||||
alt: "Zixi", click_url: "https://zixi.com/" },
|
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",
|
src: "/legacy/ads/Telycam_BroadcastBeat_300x250_MixOne-ExploreXE_with_NAB2026_2f30157f.gif",
|
||||||
alt: "Telycam MixOne / ExploreXE — 2026 NAB Show", click_url: "https://telycam.com/" },
|
click_url: "https://advertising.relevantmediaproperties.com/r/d4fda1cd-1ffc-4ae5-bec3-1b38cf62a5e9",
|
||||||
{ slug: "lectrosonics-300x250", label: "Lectrosonics", size: "300x250",
|
{ slug: "lectrosonics-300x250",
|
||||||
|
campaign_id: "e69cb845-5526-4165-bca1-964104b70c6b", label: "Lectrosonics", size: "300x250",
|
||||||
src: "/legacy/ads/300x250-banner-Our-Story-film_resize.jpg",
|
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 {
|
interface DbRow {
|
||||||
|
|||||||
Reference in New Issue
Block a user