wip-banner-system: full banner pool, stack all 300x250s, footer 728x90
Banner spec from Ryan: - 728x90 header (every page): pool is now LiveU only; rotates when more 728x90 entries are added to ads.ts. - 728x90 below main menu on article pages: removed (was the article-top AdSlot stub). - 728x90 footer (every page): replaces the old 300x250 rocket.new placeholder in Footer.tsx. Rotates from the same ADS_728X90 pool. - 300x250 sidebar: now renders EVERY entry in ADS_300X250 stacked vertically, shuffled per page-view. Pool: AJA, Zixi, Telycam, LiveU, Lectrosonics, Opengear, Studio Hero, Magewell. Adding more entries to ads.ts auto-grows the stack. - 300x600 fixed slot: Blackmagic (Adsanity ad-210571), top of sidebar, never rotated. Implementation: - ads.ts: Ad interface now allows either src (local image) or adsanity_id (iframe). New rotateAll() returns ALL ads of a size shuffled; pickAds kept for ArticleBody bounded slots. - AdImage.tsx: dropped the legacy ADSANITY_CAMPAIGN_MAP label fallback — ads.ts is now the single source of truth, so Telycam (no adsanity_id) renders its local GIF instead of the iframe. - SidebarAdStack.tsx: rotateAll(300x250) instead of pickAds with a count cap. count prop removed; callers updated. - NewsArticleDetailClient.tsx: removed article-top 728x90 stub. - Footer.tsx: 728x90 AdImage in place of the 300x250 placeholder. - LeaderboardAd.tsx: deleted (was unused after the earlier home-page cleanup). - PENDING_ads.md: rewritten — Adsanity IDs to verify (Studio Hero, Blackmagic) and optional local-file upgrades. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,41 +1,29 @@
|
||||
# Pending Ad Creatives — upload to public/legacy/ads/
|
||||
# Pending Ad Creatives
|
||||
|
||||
Images below were NOT recovered from the Wayback Machine (Wayback returned
|
||||
HTML overlays instead of raw files). Ryan needs to supply these from local
|
||||
storage or the original advertiser.
|
||||
The active banner pool is defined in `src/lib/ads.ts`. Banners with an
|
||||
`adsanity_id` render via the Adsanity iframe at
|
||||
`https://ads.broadcastbeat.com/wp-content/plugins/adsanity/render.php?id=...`
|
||||
and need no local file. Banners with `src` need the file in
|
||||
`public/legacy/ads/`.
|
||||
|
||||
## 728x90 Leaderboard (1 missing)
|
||||
- `Matrox_728x90_ConvertIP_Series_0722_7b13be6a.jpg` — Matrox ConvertIP Series
|
||||
Click URL: https://info.matrox.com/video/ad/infrastructure/broadcast/convertip-series
|
||||
## Adsanity campaign IDs to verify in WordPress admin
|
||||
- **Studio Hero** (currently `ad-162714`) — that ID was originally mapped
|
||||
to "Studio Suite"; confirm the Hero campaign exists in Adsanity and
|
||||
update `ads.ts` if a different ID is correct.
|
||||
- **Blackmagic 300x600** (currently `ad-210571`) — that ID is also used by
|
||||
the Riedel campaign in the historical map; confirm the ATEM 4 M/E
|
||||
campaign has its own ID.
|
||||
|
||||
## 300x600 Fixed Sidebar (1 missing — highest priority)
|
||||
- `ATEM-4ME-Constellation-4K-Plus-v1g-300x600-EN_1804ac7b.jpg` — Blackmagic ATEM 4 M/E
|
||||
Click URL: http://bmd.link/mRNsKu
|
||||
Note: SidebarAdStack will show nothing in the 300x600 slot until this is added.
|
||||
## Optional local-file upgrades
|
||||
These banners currently render via Adsanity iframe. To swap to a faster
|
||||
local render, drop the image into `public/legacy/ads/` and add `src` +
|
||||
`alt` + `click_url` to the corresponding entry in `src/lib/ads.ts`.
|
||||
|
||||
## 300x250 Rotating Sidebar (20 missing)
|
||||
- `Magewell_Director_One_Pro-Convert-IP-to-HDMI_Sports_InfoComm_9170d5cd.gif` — Magewell Director One
|
||||
- `300x250-banner-Our-Story-film_resize_c30978de.jpg` — Lectrosonics Our Story
|
||||
- `Zixi_Ads_300x250_93cf5742.png` — Zixi
|
||||
- `Studio-Hero-for-Broadcast-Beat_e1aaa706.png` — Studio Hero
|
||||
- `aja_2025_colorbox_og_colorbox_300x250_en-3_041eaa50.gif` — AJA ColorBox 2025
|
||||
- `1467-NAB-300x250-R2V1-1_364334f9.png` — Dell at NAB
|
||||
- `Broadcasters-report-v2-300x250-px_9b98a4c3.jpg` — JW Player Broadcasters Report
|
||||
- `HPA-ad-Oct-22_44905037.png` — Puget Systems HPA
|
||||
- `Broadcast-Beat-April-Banner-V1_160b8d08.jpg` — ROE Visual
|
||||
- `webinar-ad_19d5c0c1.png` — Viz Flowics 2023 Webinar
|
||||
- `BB_NAB1_022924-15_f127d83a.gif` — BOXX at NAB
|
||||
- `October-November-ad-Broadcast-Beat-TAG-Monitoring-Layers-blo_9d37b415.jpg` — TAG Video Systems
|
||||
- `Broadcast-Beat-Banner-March-2024-300x250-1_6dda0fb9.jpg` — Camplex SMPTE
|
||||
- `GIF3_07_10fps_03_71177a21.gif` — BOXX animated
|
||||
- `300x250_Matrox_Monarch_EDGE_Series_Flex_Your_Workflow_0924_caf31161.jpg` — Matrox Monarch EDGE
|
||||
- `quantum-webinar_8db637f8.jpg` — Quantum Webinar
|
||||
- `openGear-guide-2026-300x250-1_a5c4b515.png` — openGear Guide 2026
|
||||
- `250417-300-x-250-ad_4e4ea2cf.png` — OpenDrives NAB 2025
|
||||
- `image001-1-1_d6f7d0df.png` — Look Solutions Tiny S
|
||||
- `300x250-Bolero-Mini_4c8e5038.gif` — Riedel Bolero Mini
|
||||
|
||||
## How to restore
|
||||
1. Drop each file into `public/legacy/ads/` in the broadcastbeat repo
|
||||
2. Re-add the entry to the appropriate array in `src/lib/ads.ts`
|
||||
3. Push and Coolify will redeploy automatically
|
||||
- AJA Video 300x250
|
||||
- Zixi 300x250
|
||||
- Lectrosonics 300x250
|
||||
- Opengear 300x250
|
||||
- Studio Hero 300x250
|
||||
- Magewell 300x250
|
||||
- Blackmagic 300x600 — Wayback target file:
|
||||
`ATEM-4ME-Constellation-4K-Plus-v1g-300x600-EN.jpg`, link http://bmd.link/mRNsKu
|
||||
|
||||
@@ -900,10 +900,10 @@ export default function ArticleFeed() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sidebar — Blackmagic 300x600 fixed at top + 5 rotating 300x250s */}
|
||||
{/* Sidebar — Blackmagic 300x600 fixed at top + every 300x250 banner stacked */}
|
||||
<aside className="lg:col-span-4" aria-label="Advertisements">
|
||||
<div className="lg:sticky lg:top-24">
|
||||
<SidebarAdStack count={5} />
|
||||
<SidebarAdStack />
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import React from "react";
|
||||
import AdSlot from "@/components/AdSlot";
|
||||
|
||||
export default function LeaderboardAd() {
|
||||
return (
|
||||
<div className="bg-ice-blue border-b border-border py-3">
|
||||
<div className="max-w-container mx-auto px-4 flex justify-center">
|
||||
<AdSlot zone="homepage-top" size="728x90" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -283,11 +283,6 @@ export default function NewsArticleDetailClient({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top leaderboard */}
|
||||
<div className="max-w-container mx-auto px-4 pt-6">
|
||||
<AdSlot zone="article-top" size="728x90" />
|
||||
</div>
|
||||
|
||||
{/* Article */}
|
||||
<article className="max-w-container mx-auto px-4 py-8 md:py-12">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
@@ -453,8 +448,8 @@ export default function NewsArticleDetailClient({
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside className="lg:col-span-4 space-y-6">
|
||||
{/* Blackmagic 300x600 fixed at top + rotating 300x250 banners */}
|
||||
<SidebarAdStack count={4} />
|
||||
{/* Blackmagic 300x600 fixed at top + every 300x250 banner stacked */}
|
||||
<SidebarAdStack />
|
||||
|
||||
{/* Related Articles Sidebar */}
|
||||
<div className="bg-[#111] border border-[#222] p-5">
|
||||
|
||||
@@ -4,33 +4,19 @@ import AppImage from '@/components/ui/AppImage';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
|
||||
interface Ad {
|
||||
src: string;
|
||||
label: string;
|
||||
size: string;
|
||||
src?: string;
|
||||
alt?: string;
|
||||
click_url?: string;
|
||||
adsanity_id?: string;
|
||||
}
|
||||
|
||||
const ADSANITY_CAMPAIGN_MAP: Record<string, string> = {
|
||||
'riedel': 'ad-210571',
|
||||
'look-solutions': 'ad-208185',
|
||||
'opengear': 'ad-200174',
|
||||
'telycam': 'ad-191740',
|
||||
'livu-728': 'ad-189081',
|
||||
'aja': 'ad-164285',
|
||||
'studio-suite': 'ad-162714',
|
||||
'zixi': 'ad-156737',
|
||||
'lectrosonics': 'ad-156727',
|
||||
'livu-300': 'ad-156486',
|
||||
'magewell': 'ad-154296',
|
||||
'blackmagic': 'ad-210571'
|
||||
};
|
||||
|
||||
export default function AdImage({ ad, page, priority }: { ad: Ad; page?: string; priority?: boolean }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const trackedRef = useRef(false);
|
||||
const supabase = createClient();
|
||||
const adsanityId = ad.adsanity_id || ADSANITY_CAMPAIGN_MAP[ad.label.toLowerCase().replace(/\s+/g, '-')] || '';
|
||||
const adsanityId = ad.adsanity_id || '';
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
@@ -74,12 +60,14 @@ export default function AdImage({ ad, page, priority }: { ad: Ad; page?: string;
|
||||
);
|
||||
}
|
||||
|
||||
if (!ad.src) return null;
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="ad-container">
|
||||
{ad.click_url ? (
|
||||
<a href={ad.click_url} rel="sponsored noopener noreferrer" onClick={handleClick}><AppImage src={ad.src} alt={ad.label} width={width} height={height} priority={priority} /></a>
|
||||
<a href={ad.click_url} rel="sponsored noopener noreferrer" onClick={handleClick}><AppImage src={ad.src} alt={ad.alt || ad.label} width={width} height={height} priority={priority} /></a>
|
||||
) : (
|
||||
<AppImage src={ad.src} alt={ad.label} width={width} height={height} priority={priority} />
|
||||
<AppImage src={ad.src} alt={ad.alt || ad.label} width={width} height={height} priority={priority} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
TwitterXIcon,
|
||||
FacebookIcon } from
|
||||
"@/components/ui/Icons";
|
||||
import AdImage from "@/components/AdImage";
|
||||
import { ADS_728X90, shuffle } from "@/lib/ads";
|
||||
|
||||
// DO NOT OVERRIDE — Footer column structure and link targets
|
||||
const footerColumns = [
|
||||
@@ -61,20 +63,13 @@ const footerColumns = [
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-[#0d0d0d] border-t border-[#222]">
|
||||
{/* Footer Ad Row */}
|
||||
{/* Footer Ad Row — site-wide 728x90 (rotates if multiple in pool) */}
|
||||
<div className="max-w-container mx-auto px-4 pt-8 pb-4">
|
||||
<div className="flex flex-wrap justify-center gap-4 mb-8">
|
||||
{/* DO NOT OVERRIDE — footer ad placeholder */}
|
||||
<div className="ad-placeholder w-[300px] h-[250px]">
|
||||
<AppImage
|
||||
src="https://img.rocket.new/generatedImages/rocket_gen_img_189430e3f-1768594771653.png"
|
||||
alt="Advertisement — BroadcastBeat sponsor"
|
||||
width={300}
|
||||
height={250}
|
||||
className="w-full h-full object-contain" />
|
||||
|
||||
{ADS_728X90.length > 0 && (
|
||||
<div className="flex justify-center mb-8">
|
||||
<AdImage ad={shuffle(ADS_728X90)[0]} page="footer" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Multi-column link section */}
|
||||
<div className="border-t border-[#222] pt-8 pb-6 grid grid-cols-2 md:grid-cols-4 gap-8">
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
"use client";
|
||||
import { useMemo } from "react";
|
||||
import AdImage from "./AdImage";
|
||||
import { FIXED_300X600, pickAds, type Ad } from "@/lib/ads";
|
||||
import { FIXED_300X600, rotateAll, type Ad } from "@/lib/ads";
|
||||
|
||||
// Renders the fixed 300x600 at the top (no rotation) followed by EVERY
|
||||
// 300x250 banner stacked vertically in a shuffled order. Adding more
|
||||
// 300x250 entries to ads.ts automatically grows this stack.
|
||||
export default function SidebarAdStack({
|
||||
count = 5,
|
||||
excludeSrcs = [],
|
||||
className = "",
|
||||
}: {
|
||||
count?: number;
|
||||
excludeSrcs?: string[];
|
||||
className?: string;
|
||||
}) {
|
||||
const rotating = useMemo<Ad[]>(
|
||||
() => pickAds("300x250", count, new Set(excludeSrcs)),
|
||||
[count, excludeSrcs.join("|")],
|
||||
() => rotateAll("300x250", new Set(excludeSrcs)),
|
||||
[excludeSrcs.join("|")],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`space-y-4 ${className}`}>
|
||||
{FIXED_300X600 && <AdImage ad={FIXED_300X600} priority />}
|
||||
{rotating.map((ad) => (
|
||||
<AdImage key={ad.src} ad={ad} />
|
||||
<AdImage key={ad.src ?? ad.adsanity_id ?? ad.label} ad={ad} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
123
src/lib/ads.ts
123
src/lib/ads.ts
@@ -1,32 +1,92 @@
|
||||
// Ad creatives with verified image files in public/legacy/ads/.
|
||||
// Only ads with a real downloaded image are included — entries without
|
||||
// a recovered file have been moved to PENDING.md for Ryan to upload.
|
||||
// Missing: Matrox 728x90, Blackmagic 300x600, and 20 of 22 300x250s.
|
||||
// Banner control panel: this file is the source of truth for which ads
|
||||
// render on broadcastbeat.com. Adding a new entry here automatically
|
||||
// rotates it into the appropriate zone — no other code changes needed.
|
||||
//
|
||||
// Each Ad must provide EITHER:
|
||||
// - src + click_url + alt → local image, click goes to click_url
|
||||
// - adsanity_id → Adsanity iframe (Adsanity tracks the click)
|
||||
|
||||
export interface Ad {
|
||||
src: string;
|
||||
alt: string;
|
||||
label: string;
|
||||
size: "300x250" | "300x600" | "728x90";
|
||||
click_url: string;
|
||||
src?: string;
|
||||
alt?: string;
|
||||
click_url?: string;
|
||||
adsanity_id?: string;
|
||||
}
|
||||
|
||||
/** Rotating leaderboard pool — 3 verified real images. */
|
||||
/** 728x90 leaderboard pool — header (top of every page) and footer. */
|
||||
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/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" },
|
||||
{
|
||||
label: "LiveU",
|
||||
size: "728x90",
|
||||
src: "/legacy/ads/728-x-90-pxEN_3ce6f379.gif",
|
||||
alt: "LiveU 728x90",
|
||||
click_url: "https://bit.ly/4ghXVeq",
|
||||
},
|
||||
];
|
||||
|
||||
/** Rotating 300×250 sidebar pool — 2 verified real images. */
|
||||
/**
|
||||
* 300x250 sidebar pool. ALL entries render on every page — SidebarAdStack
|
||||
* stacks them vertically and shuffles the order per page-view so each banner
|
||||
* cycles through positions. Add new entries here to grow the stack.
|
||||
*/
|
||||
export const ADS_300X250: Ad[] = [
|
||||
{ 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/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/" },
|
||||
{
|
||||
label: "AJA Video",
|
||||
size: "300x250",
|
||||
adsanity_id: "ad-164285",
|
||||
},
|
||||
{
|
||||
label: "Zixi",
|
||||
size: "300x250",
|
||||
adsanity_id: "ad-156737",
|
||||
},
|
||||
{
|
||||
label: "Telycam",
|
||||
size: "300x250",
|
||||
src: "/legacy/ads/Telycam_BroadcastBeat_300x250_MixOne-ExploreXE_with_NAB2026_2f30157f.gif",
|
||||
alt: "Telycam Mix One ExploreXE NAB 2026",
|
||||
click_url: "https://telycam.com/",
|
||||
},
|
||||
{
|
||||
label: "LiveU",
|
||||
size: "300x250",
|
||||
src: "/legacy/ads/PAYG-300x250-1_31957672.gif",
|
||||
alt: "LiveU pay-as-you-go",
|
||||
click_url: "https://bit.ly/4ghXVeq",
|
||||
},
|
||||
{
|
||||
label: "Lectrosonics",
|
||||
size: "300x250",
|
||||
adsanity_id: "ad-156727",
|
||||
},
|
||||
{
|
||||
label: "Opengear",
|
||||
size: "300x250",
|
||||
adsanity_id: "ad-200174",
|
||||
},
|
||||
{
|
||||
label: "Studio Hero",
|
||||
size: "300x250",
|
||||
adsanity_id: "ad-162714",
|
||||
},
|
||||
{
|
||||
label: "Magewell",
|
||||
size: "300x250",
|
||||
adsanity_id: "ad-154296",
|
||||
},
|
||||
];
|
||||
|
||||
// 300x600 fixed sidebar: Blackmagic image not recovered from archive.
|
||||
// SidebarAdStack skips this slot when null.
|
||||
export const FIXED_300X600: Ad | null = null;
|
||||
/**
|
||||
* 300x600 fixed sidebar slot. Rendered at the TOP of SidebarAdStack and is
|
||||
* NEVER rotated. Set to null to omit. To replace, swap this single entry.
|
||||
*/
|
||||
export const FIXED_300X600: Ad | null = {
|
||||
label: "Blackmagic",
|
||||
size: "300x600",
|
||||
adsanity_id: "ad-210571",
|
||||
};
|
||||
|
||||
/** Fisher-Yates shuffle, returns a new array. */
|
||||
export function shuffle<T>(arr: T[]): T[] {
|
||||
@@ -39,19 +99,30 @@ export function shuffle<T>(arr: T[]): T[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Return ALL ads of the requested size in a shuffled order. Use this when
|
||||
* every banner must render (e.g. the sidebar 300x250 stack); the shuffle
|
||||
* ensures each banner cycles through positions across page-views.
|
||||
*/
|
||||
export function rotateAll(size: Ad["size"], exclude: Set<string> = new Set()): Ad[] {
|
||||
const pool =
|
||||
size === "300x250" ? ADS_300X250
|
||||
: size === "728x90" ? ADS_728X90
|
||||
: size === "300x600" ? (FIXED_300X600 ? [FIXED_300X600] : [])
|
||||
: [];
|
||||
const key = (a: Ad) => a.src ?? a.adsanity_id ?? a.label;
|
||||
return shuffle(pool.filter((a) => !exclude.has(key(a))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw N ads of the requested size, shuffled. Used by ArticleBody for
|
||||
* a bounded number of in-article slots. For zones that must show every
|
||||
* banner, prefer rotateAll().
|
||||
*/
|
||||
export function pickAds(
|
||||
size: Ad["size"],
|
||||
n: number,
|
||||
exclude: Set<string> = new Set(),
|
||||
): Ad[] {
|
||||
const pool =
|
||||
size === "300x250" ? ADS_300X250
|
||||
: size === "728x90" ? ADS_728X90
|
||||
: [];
|
||||
const filtered = pool.filter((a) => !exclude.has(a.src));
|
||||
return shuffle(filtered).slice(0, Math.min(n, filtered.length));
|
||||
const all = rotateAll(size, exclude);
|
||||
return all.slice(0, Math.min(n, all.length));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user