From 9c5b0e4959900943ce5729fe86980475e29e6630 Mon Sep 17 00:00:00 2001 From: Ryan Salazar Date: Fri, 8 May 2026 18:16:07 +0000 Subject: [PATCH] wire real ad rotation: Blackmagic 300x600 fixed + rotating 300x250s Replace the placeholder Unsplash banner array with the live AdSanity inventory served from /legacy/ads/ (same 27 active creatives currently on broadcastbeat.com). New: - src/lib/ads.ts: ad inventory (1 fixed Blackmagic 300x600, 22 rotating 300x250s, 4 leaderboard 728x90s) + Fisher-Yates shuffle + pickAds() helper that draws N unique ads excluding any caller-supplied src set. - src/components/AdImage.tsx: renders one ad as . - src/components/SidebarAdStack.tsx: Blackmagic 300x600 fixed on top, then count rotating 300x250s. Memoizes the rotation per render so a refresh re-shuffles. Accepts excludeSrcs to coordinate with other slots on the same page. - src/components/ArticleBody.tsx: splits article HTML at

boundaries, splices a 300x250 ad after every Nth paragraph (default 4). Same exclude-set pattern. Wiring: - ArticleFeed.tsx: remove the inline sidebarAds array (was 5 Unsplash placeholders + 1 LiveU 300x250). Sidebar now . - NewsArticleDetailClient.tsx: sidebar 300x600 + 300x250 AdSlot placeholders replaced with . Article body innerHTML replaced with so 300x250 ads slot in between paragraphs. Same banners as the live broadcastbeat.com (sourced from the existing WP AdSanity install we cached locally during Phase 2). Each
opens in a new tab and uses rel='sponsored noopener noreferrer'. --- src/app/home-page/components/ArticleFeed.tsx | 52 +----------- .../news/[slug]/NewsArticleDetailClient.tsx | 14 ++-- src/components/AdImage.tsx | 39 +++++++++ src/components/ArticleBody.tsx | 68 +++++++++++++++ src/components/SidebarAdStack.tsx | 36 ++++++++ src/lib/ads.ts | 82 +++++++++++++++++++ 6 files changed, 236 insertions(+), 55 deletions(-) create mode 100644 src/components/AdImage.tsx create mode 100644 src/components/ArticleBody.tsx create mode 100644 src/components/SidebarAdStack.tsx create mode 100644 src/lib/ads.ts diff --git a/src/app/home-page/components/ArticleFeed.tsx b/src/app/home-page/components/ArticleFeed.tsx index d8d734d..0ace312 100644 --- a/src/app/home-page/components/ArticleFeed.tsx +++ b/src/app/home-page/components/ArticleFeed.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useState, useRef, useId, useEffect, useCallback } from "react"; import AppImage from "@/components/ui/AppImage"; +import SidebarAdStack from "@/components/SidebarAdStack"; import { PersonIcon, ChevronLeftIcon, ChevronRightIcon, SearchIcon, CloseIcon } from "@/components/ui/Icons"; import Link from "next/link"; @@ -202,14 +203,6 @@ const staticArticles: ArticleItem[] = [ } ]; -const sidebarAds = [ - { src: "https://images.unsplash.com/photo-1611532736597-de2d4265fba3?w=300&h=600&fit=crop", alt: "Blackmagic Design advertisement 300x600", label: "Blackmagic", size: "300x600" }, - { src: "https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=300&h=250&fit=crop", alt: "Zixi broadcast technology advertisement 300x250", label: "Zixi", size: "300x250" }, - { src: "https://www.broadcastbeat.com/wp-content/uploads/728-x-90-pxEN.gif", alt: "LiveU live production solutions advertisement 300x250", label: "LiveU", size: "300x250" }, - { src: "https://images.unsplash.com/photo-1563986768609-322da13575f3?w=300&h=250&fit=crop", alt: "TelyCam broadcast camera advertisement 300x250", label: "TelyCam", size: "300x250" }, - { src: "https://images.unsplash.com/photo-1574717024653-61fd2cf4d44d?w=300&h=250&fit=crop", alt: "AJA Video Systems advertisement 300x250", label: "AJA", size: "300x250" }, - { src: "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=300&h=250&fit=crop", alt: "Studio Suite production management advertisement 300x250", label: "Studio Suite", size: "300x250" } -]; const ALL_CATEGORIES = ["All", "BROADCAST", "FEATURED", "POST PRODUCTION", "ANIMATION"]; const SOURCE_FILTERS = ["All", "Live", "Imported"] as const; @@ -907,49 +900,10 @@ export default function ArticleFeed() { )} - {/* Sidebar */} + {/* Sidebar — Blackmagic 300x600 fixed at top + 5 rotating 300x250s */} diff --git a/src/app/news/[slug]/NewsArticleDetailClient.tsx b/src/app/news/[slug]/NewsArticleDetailClient.tsx index a24b670..04fb9ea 100644 --- a/src/app/news/[slug]/NewsArticleDetailClient.tsx +++ b/src/app/news/[slug]/NewsArticleDetailClient.tsx @@ -5,6 +5,8 @@ import AppImage from "@/components/ui/AppImage"; import Header from "@/components/Header"; import Footer from "@/components/Footer"; import AdSlot from "@/components/AdSlot"; +import SidebarAdStack from "@/components/SidebarAdStack"; +import ArticleBody from "@/components/ArticleBody"; import { createClient } from "@/lib/supabase/client"; import type { Article } from "@/lib/articles/types"; @@ -377,8 +379,10 @@ export default function NewsArticleDetailClient({ /> - {/* Article Body */} -
{/* Tags */} @@ -450,8 +453,8 @@ export default function NewsArticleDetailClient({ {/* Sidebar */}
diff --git a/src/components/AdImage.tsx b/src/components/AdImage.tsx new file mode 100644 index 0000000..cd010d0 --- /dev/null +++ b/src/components/AdImage.tsx @@ -0,0 +1,39 @@ +import AppImage from "@/components/ui/AppImage"; +import type { Ad } from "@/lib/ads"; + +const SIZE_DIM: Record = { + "300x250": { w: 300, h: 250 }, + "300x600": { w: 300, h: 600 }, + "728x90": { w: 728, h: 90 }, +}; + +export default function AdImage({ + ad, + className = "", + priority = false, +}: { + ad: Ad; + className?: string; + priority?: boolean; +}) { + const { w, h } = SIZE_DIM[ad.size]; + return ( + + + + ); +} diff --git a/src/components/ArticleBody.tsx b/src/components/ArticleBody.tsx new file mode 100644 index 0000000..2e6ff73 --- /dev/null +++ b/src/components/ArticleBody.tsx @@ -0,0 +1,68 @@ +"use client"; +import { useMemo } from "react"; +import AdImage from "./AdImage"; +import { pickAds } from "@/lib/ads"; + +/** + * Article body renderer: splits the supplied HTML at paragraph boundaries + * and splices a 300×250 ad after every Nth paragraph (default 4). + * + * `excludeSrcs` lets the caller forbid creatives already used in the + * sidebar so the same page never shows a banner twice. + */ +export default function ArticleBody({ + html, + everyN = 4, + excludeSrcs = [], + className = "", +}: { + html: string; + everyN?: number; + excludeSrcs?: string[]; + className?: string; +}) { + const { chunks, ads } = useMemo(() => { + // Split keeping the closing

tags so each chunk is one paragraph block. + const parts = (html || "").split(/(<\/p>)/i); + const grouped: string[] = []; + let buf = ""; + for (const p of parts) { + buf += p; + if (p.toLowerCase() === "

") { + grouped.push(buf); + buf = ""; + } + } + if (buf.trim()) grouped.push(buf); + + const slotsNeeded = Math.max(0, Math.floor((grouped.length - 1) / everyN)); + const ads = pickAds("300x250", slotsNeeded, new Set(excludeSrcs)); + return { chunks: grouped, ads }; + }, [html, everyN, excludeSrcs.join("|")]); + + const out: React.ReactNode[] = []; + let adCursor = 0; + for (let i = 0; i < chunks.length; i++) { + out.push( +
, + ); + // Insert ad after this chunk if it's at an everyN boundary and not the last + if ((i + 1) % everyN === 0 && i < chunks.length - 1 && adCursor < ads.length) { + const ad = ads[adCursor++]; + out.push( +
+ +
, + ); + } + } + + return
{out}
; +} diff --git a/src/components/SidebarAdStack.tsx b/src/components/SidebarAdStack.tsx new file mode 100644 index 0000000..d51d462 --- /dev/null +++ b/src/components/SidebarAdStack.tsx @@ -0,0 +1,36 @@ +"use client"; +import { useMemo } from "react"; +import AdImage from "./AdImage"; +import { FIXED_BLACKMAGIC_300X600, pickAds, type Ad } from "@/lib/ads"; + +/** + * Sidebar ad stack — fixed Blackmagic 300×600 on top, then `count` rotating + * 300×250 banners. + * + * Pass `excludeSrcs` (Set or array) when other slots on the same page already + * use specific 300×250 creatives, to avoid duplicates within a page render. + */ +export default function SidebarAdStack({ + count = 5, + excludeSrcs = [], + className = "", +}: { + count?: number; + excludeSrcs?: string[]; + className?: string; +}) { + const rotating = useMemo( + () => pickAds("300x250", count, new Set(excludeSrcs)), + // exclude list is page-render scoped; recompute when it changes + [count, excludeSrcs.join("|")], + ); + + return ( +
+ + {rotating.map((ad) => ( + + ))} +
+ ); +} diff --git a/src/lib/ads.ts b/src/lib/ads.ts new file mode 100644 index 0000000..9d3e95a --- /dev/null +++ b/src/lib/ads.ts @@ -0,0 +1,82 @@ +// Live ad creatives — same set served by the existing broadcastbeat.com +// AdSanity install. Sourced from /legacy/ads/ on the persistent storage +// volume; click-through URLs preserved from the original WP postmeta. + +export interface Ad { + src: string; + alt: string; + label: string; + size: "300x250" | "300x600" | "728x90"; + click_url: string; +} + +/** Fixed sidebar half-page that always renders at the top across the site. */ +export const FIXED_BLACKMAGIC_300X600: Ad = { + src: "/legacy/ads/ATEM-4ME-Constellation-4K-Plus-v1g-300x600-EN_1804ac7b.jpg", + alt: "Blackmagic Design ATEM 4 M/E Constellation 4K Plus", + label: "Blackmagic", + size: "300x600", + click_url: "http://bmd.link/mRNsKu", +}; + +/** Rotating 300×250 inventory — sidebar mid + inline article slots. */ +export const ADS_300X250: Ad[] = [ + { src: "/legacy/ads/Magewell_Director_One_Pro-Convert-IP-to-HDMI_Sports_InfoComm_9170d5cd.gif", alt: "Magewell Director One Pro", label: "Magewell", size: "300x250", click_url: "https://www.magewell.com" }, + { src: "/legacy/ads/PAYG-300x250-1_31957672.gif", alt: "LiveU pay-as-you-go", label: "LiveU", size: "300x250", click_url: "https://bit.ly/4ghXVeq" }, + { src: "/legacy/ads/300x250-banner-Our-Story-film_resize_c30978de.jpg", alt: "Lectrosonics — Our Story", label: "Lectrosonics", size: "300x250", click_url: "https://bit.ly/3PWTsSV" }, + { src: "/legacy/ads/Zixi_Ads_300x250_93cf5742.png", alt: "Zixi", label: "Zixi", size: "300x250", click_url: "https://zixi.com/" }, + { src: "/legacy/ads/Studio-Hero-for-Broadcast-Beat_e1aaa706.png", alt: "Studio Hero", label: "Studio Hero", size: "300x250", click_url: "http://www.TheStudioHero.com" }, + { src: "/legacy/ads/aja_2025_colorbox_og_colorbox_300x250_en-3_041eaa50.gif", alt: "AJA ColorBox", label: "AJA", size: "300x250", click_url: "https://www.aja.com/solutions/color" }, + { src: "/legacy/ads/1467-NAB-300x250-R2V1-1_364334f9.png", alt: "Dell at NAB", label: "Dell", size: "300x250", click_url: "https://www.dell.com/en-us/dt/events/media/index.htm" }, + { src: "/legacy/ads/Broadcasters-report-v2-300x250-px_9b98a4c3.jpg", alt: "JW Player Broadcasters Report", label: "JW Player", size: "300x250", click_url: "https://info.jwplayer.com/l/927863/2022-10-24/j4g5v" }, + { src: "/legacy/ads/HPA-ad-Oct-22_44905037.png", alt: "Puget Systems HPA", label: "Puget", size: "300x250", click_url: "http://www.pugetsystems.com" }, + { src: "/legacy/ads/Broadcast-Beat-April-Banner-V1_160b8d08.jpg", alt: "ROE Visual", label: "ROE", size: "300x250", click_url: "https://www.roevisual.com/" }, + { src: "/legacy/ads/webinar-ad_19d5c0c1.png", alt: "Viz Flowics 2023 Webinar", label: "Viz Flowics", size: "300x250", click_url: "https://broadcastbeat.webinargeek.com/viz-flowics-2023" }, + { src: "/legacy/ads/BB_NAB1_022924-15_f127d83a.gif", alt: "Boxx at NAB", label: "Boxx", size: "300x250", click_url: "https://boxx.com/nab2024" }, + { src: "/legacy/ads/October-November-ad-Broadcast-Beat-TAG-Monitoring-Layers-blo_9d37b415.jpg", alt: "TAG Video Systems", label: "TAG", size: "300x250", click_url: "https://tagvs.com/blog/future-proofing-broadcast-operations-monitoring/" }, + { src: "/legacy/ads/Broadcast-Beat-Banner-March-2024-300x250-1_6dda0fb9.jpg", alt: "Camplex SMPTE", label: "Camplex", size: "300x250", click_url: "https://www.camplex.com/products/category/smpte" }, + { src: "/legacy/ads/GIF3_07_10fps_03_71177a21.gif", alt: "Boxx animated", label: "Boxx", size: "300x250", click_url: "https://boxx.com/nab2024" }, + { src: "/legacy/ads/Telycam_BroadcastBeat_300x250_MixOne-ExploreXE_with_NAB2026_2f30157f.gif", alt: "Telycam Mix One ExploreXE NAB 2026", label: "Telycam", size: "300x250", click_url: "https://telycam.com/" }, + { src: "/legacy/ads/300x250_Matrox_Monarch_EDGE_Series_Flex_Your_Workflow_0924_caf31161.jpg", alt: "Matrox Monarch EDGE", label: "Matrox", size: "300x250", click_url: "https://go.matrox.com/transport-contact-us.html" }, + { src: "/legacy/ads/quantum-webinar_8db637f8.jpg", alt: "Quantum Webinar", label: "Quantum", size: "300x250", click_url: "https://broadcastbeat.easywebinar.live/event-registration" }, + { src: "/legacy/ads/openGear-guide-2026-300x250-1_a5c4b515.png", alt: "openGear Guide 2026", label: "openGear", size: "300x250", click_url: "https://www.opengear.tv/ultimate-application-guide/" }, + { src: "/legacy/ads/250417-300-x-250-ad_4e4ea2cf.png", alt: "OpenDrives at NAB 2025", label: "OpenDrives", size: "300x250", click_url: "https://opendrives.ac-page.com/nabshow2025" }, + { src: "/legacy/ads/image001-1-1_d6f7d0df.png", alt: "Look Solutions Tiny S", label: "Look Solutions", size: "300x250", click_url: "https://looksolutionsusa.com/product/tiny-s/" }, + { src: "/legacy/ads/300x250-Bolero-Mini_4c8e5038.gif", alt: "Riedel Bolero Mini", label: "Riedel", size: "300x250", click_url: "https://www.riedel.net/en/" }, +]; + +/** Leaderboards — used by AdSlot zones article-top / article-bottom / homepage-top. */ +export const ADS_728X90: Ad[] = [ + { src: "/legacy/ads/Broadcast-Beat-Ad-March-2023_2f1904b5.png", alt: "Tower Products", label: "Tower", size: "728x90", click_url: "https://bit.ly/3rd4oBS" }, + { src: "/legacy/ads/Matrox_728x90_ConvertIP_Series_0722_7b13be6a.jpg", alt: "Matrox ConvertIP", label: "Matrox", size: "728x90", click_url: "https://info.matrox.com/video/ad/infrastructure/broadcast/convertip-series" }, + { src: "/legacy/ads/11503_URXP41D_Audio_728X90_OnlineBanner_V1_StaticBackup_40K_f92207ce.jpg", alt: "Sony Pro UWP-D27", label: "Sony", size: "728x90", click_url: "https://pro.sony/ue_US/products/broadcastaudiouwpdseriesmicpackages/uwp-d27" }, + { src: "/legacy/ads/728-x-90-pxEN_3ce6f379.gif", alt: "LiveU 728x90", label: "LiveU", size: "728x90", click_url: "https://bit.ly/4ghXVeq" }, +]; + +/** Fisher-Yates shuffle, returns a new array. */ +export function shuffle(arr: T[]): T[] { + const out = arr.slice(); + for (let i = out.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [out[i], out[j]] = [out[j], out[i]]; + } + return out; +} + +/** + * Draw N unique ads of the requested size for a single page render. + * Pass `exclude` (set of `src` strings already used elsewhere on the same page) + * to guarantee no duplicates across slots. + */ +export function pickAds( + size: Ad["size"], + n: number, + exclude: Set = new Set(), +): Ad[] { + const pool = + size === "300x250" ? ADS_300X250 + : size === "728x90" ? ADS_728X90 + : []; // 300x600 is the fixed Blackmagic; not pooled + const filtered = pool.filter((a) => !exclude.has(a.src)); + return shuffle(filtered).slice(0, Math.min(n, filtered.length)); +}