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

View File

@@ -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() {
)}
</div>
{/* Sidebar */}
{/* Sidebar — Blackmagic 300x600 fixed at top + 5 rotating 300x250s */}
<aside className="lg:col-span-4" aria-label="Advertisements">
<div className="lg:sticky lg:top-24">
<div className="grid grid-cols-2 gap-3 lg:hidden">
{sidebarAds?.filter(ad => ad?.size !== "300x600")?.map((ad, i) => (
<a key={i} href="#" aria-label={`${ad?.label} advertisement`} className="block focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]">
<div className="ad-placeholder overflow-hidden h-[120px] w-full relative">
<AppImage
src={ad?.src}
alt={ad?.alt}
fill
className="w-full h-full object-cover"
sizes="(max-width: 1024px) 50vw"
/>
<div className="absolute bottom-1 right-1 bg-black/40 text-white/60 font-body text-[9px] px-1 py-0.5">
AD · {ad?.label}
</div>
</div>
</a>
))}
</div>
<div className="hidden lg:flex lg:flex-col lg:space-y-4">
{sidebarAds?.map((ad, i) => (
<a key={i} href="#" aria-label={`${ad?.label} advertisement`} className="block focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]">
<div
className={`ad-placeholder overflow-hidden ${ad?.size === "300x600" ? "h-[600px]" : "h-[250px]"}`}
style={{ width: "100%", maxWidth: 300 }}>
<AppImage
src={ad?.src}
alt={ad?.alt}
width={300}
height={ad?.size === "300x600" ? 600 : 250}
className="w-full h-full object-cover"
sizes="300px"
/>
<div className="absolute bottom-1 right-1 bg-black/40 text-white/60 font-body text-[9px] px-1 py-0.5">
AD · {ad?.label}
</div>
</div>
</a>
))}
</div>
<SidebarAdStack count={5} />
</div>
</aside>
</div>

View File

@@ -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({
/>
</div>
{/* Article Body */}
<div
{/* Article Body — paragraphs interleaved with rotating 300×250 ads every 4 paragraphs */}
<ArticleBody
html={article.content}
everyN={4}
className="prose prose-invert max-w-none mb-8 font-body text-[#aaa] leading-relaxed
[&_h2]:font-heading [&_h2]:text-[#e0e0e0] [&_h2]:text-xl [&_h2]:font-bold [&_h2]:mt-8 [&_h2]:mb-4 [&_h2]:border-b [&_h2]:border-[#222] [&_h2]:pb-2
[&_p]:mb-4 [&_p]:leading-relaxed
@@ -386,7 +390,6 @@ export default function NewsArticleDetailClient({
[&_li]:text-[#aaa]
[&_strong]:text-[#e0e0e0] [&_strong]:font-bold
[&_a]:text-[#3b82f6] [&_a]:hover:underline"
dangerouslySetInnerHTML={{ __html: article.content }}
/>
{/* Tags */}
@@ -450,8 +453,8 @@ export default function NewsArticleDetailClient({
{/* Sidebar */}
<aside className="lg:col-span-4 space-y-6">
{/* Sidebar fixed half-page */}
<AdSlot zone="article-sidebar-top" size="300x600" />
{/* Blackmagic 300x600 fixed at top + rotating 300x250 banners */}
<SidebarAdStack count={4} />
{/* Related Articles Sidebar */}
<div className="bg-[#111] border border-[#222] p-5">
@@ -500,7 +503,6 @@ export default function NewsArticleDetailClient({
</div>
{/* Ad placeholder */}
<AdSlot zone="article-sidebar-mid" size="300x250" />
</aside>
</div>
</article>