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 <a target=_blank rel=
  sponsored noopener noreferrer><AppImage>.
- 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 </p>
  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 <SidebarAdStack count=5/>.
- NewsArticleDetailClient.tsx: sidebar 300x600 + 300x250 AdSlot
  placeholders replaced with <SidebarAdStack count=4/>. Article body
  innerHTML replaced with <ArticleBody html=... everyN=4/> 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 <a> opens
in a new tab and uses rel='sponsored noopener noreferrer'.
This commit is contained in:
Ryan Salazar
2026-05-08 18:16:07 +00:00
parent 7c9d2c8c79
commit 9c5b0e4959
6 changed files with 236 additions and 55 deletions

82
src/lib/ads.ts Normal file
View File

@@ -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<T>(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<string> = 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));
}