Authors page was rendering a hardcoded mock article list per slug,
with Ryan Salazar's slug falling through to the staff-reporter mock
list (847 fake articles). New behaviour:
- GET /api/author/[slug] returns ALL real articles where
bb.wp_imported_posts.author_name matches the canonical name(s)
for that slug. Slug → names map:
ryan-salazar → ["Ryan Salazar"]
james-whitfield → ["James Whitfield"]
sarah-chen → ["Sarah Chen"]
… etc.
Unknown slugs fall back to a Title-Cased guess.
Response: { total, articles[] }, cached 5min / SWR 10min.
- /authors/[slug] now fetches that endpoint on mount and replaces
the hardcoded list with the live one. Article count shown on the
profile header + sidebar is now the live total (not the
hardcoded 1200/847/etc).
- Version badge in the status bar bumped v4.2.1 → v2.0.0 to match
the actual public release.
Ryan Salazar's profile now correctly shows his 135 real bylines from
WordPress instead of the staff-reporter mock 847.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/authors/ryan-salazar was falling through to the generic
'staff-reporter' entry — which is why the page rendered as
"Broadcast Beat / Broadcast Beat Editorial Team / Joined January
2020". Add a proper ryan-salazar entry to the authors map:
name Ryan Salazar
title Founder of Broadcast Beat
joinedDate December 2013
bio (~150 words, sourced from public bios on theorg.com,
JVC casebook, AV Beat, Broadcast Beat Q&A — covers
ProComm 1997 → SunSpots CTO → AV Beat 2005 → SMPTE
LIVE 2014 → Broadcast Beat 2013 → Broadcast Beat
Studios 2020 → NAB Show LIVE executive producer.)
Also corrected the staff-reporter joinedDate to "December 2013"
(matches actual founding date of the publication).
Separately: bb.user_profiles.role for Ryan was 'administrator' and
has been updated to 'founder' so any role-based displays show the
right title.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- searchLegacyArticles() default limit: 50 → 5000. /search page
explicitly passes 5000 too. ilike on title/excerpt/author is fast
enough on the 29k-row archive that returning everything is fine.
- New API route /api/search/suggest?q=...&limit=8 returns the top
matching article titles + category + slug, with a 30-second CDN
cache + 60-second stale-while-revalidate.
- The search input in the Browse bar now debounces (150ms) and
fires that API per keystroke once 2+ chars are typed. Matches
appear in a styled dropdown below the input — click to navigate
directly to /news/<slug>, or press Enter to fall through to the
full /search page. "See all results for …" link in the dropdown
footer.
- Keyboard: Escape closes the dropdown; aria-autocomplete + role
listbox/option for accessibility.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the AI re-categorization, bb.wp_imported_posts.category holds the
new taxonomy: 'Industry News', 'Show Coverage', 'Cameras & Capture',
'Streaming & IP', 'Audio', 'Live Production', 'Cloud & Workflow',
'Post Production', 'Broadcast Production', 'AI & Automation'.
The map that drives /gear, /technology, /show-coverage, /news page
queries still referenced the OLD taxonomy ('Gear', 'Reviews',
'Technology', 'Streaming') — so /gear and /technology effectively
returned nothing-matched and fell through to the catch-all section,
causing the duplicate-posts-across-sections behaviour the user
reported.
/gear → Cameras & Capture, Audio
/technology → Streaming & IP, Cloud & Workflow, AI & Automation,
Live Production, Post Production, Broadcast Production
/show-coverage→ Show Coverage
/news → Industry News
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Featured-story card's right edge wasn't lining up with any of the
ad banners below or beside it on the homepage. Two root causes:
1. Pages I redesigned (/about, /about/contact, /about/team,
/about/advertise, /show-coverage/[slug]) used `max-w-6xl px-6`
(1152px / 24px-pad). Every other page uses `max-w-container
px-4` (1200px / 16px-pad). Pages didn't line up with the
header leaderboard or with each other.
2. ArticleFeed used a flexible 12-col grid where lg:col-span-4
ends up ~280px wide (depending on container) — so the 300x250
image inside the sidebar didn't extend to the column's right
edge, leaving a visual gap relative to the full-width Featured
card above.
Fix:
- All redesigned pages now use `max-w-container mx-auto px-4`.
- ArticleFeed switches to `lg:grid-cols-[1fr,300px]` so the sidebar
column is *exactly* 300px wide. The 300x250 ads fill that column
flush, so their right edge meets the container's right padding
edge — same x-position as the Featured card's right edge above.
Also linkified the "Relevant Media Properties" copyright string in
Footer.tsx → opens https://www.relevantmediaproperties.com/ in a new
tab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Forum:
- /forum landing: switch to 2-col layout, add SidebarAdStack
(300x600 + every 300x250) on the right.
- /forum/thread/[id]: insert one 300x250 "Sponsored" tile after
every 4 replies (idx 3, 7, 11 …). Trailing-ad guard so the last
reply isn't followed by an ad with no replies after it. Cycles
through the full ADS_300X250 pool by index so every banner gets
impressions across long threads.
Newsletter:
- /newsletter/archive: drop SidebarAdStack into the existing
right rail above the signup CTA + topic browser.
LanguageSwitcher autodetect (US default for US users was failing):
1. Timezone override: if the user is in America/*, Pacific/Honolulu,
Pacific/Auckland|Wellington, Australia/*, Europe/London|Dublin,
or Atlantic/Reykjavik — always return 'en'. Fixes US-based
users whose OS locale happened to be set to a non-English value.
2. Otherwise walk navigator.languages (the full preference list,
not just navigator.language[0]) and pick the first base code we
support (en/es/pt/fr/de/zh/ja/ar/hi).
3. Fallback to 'en'.
All non-en selections still route through Google Translate's hosted
proxy on the .translate.goog subdomain — search engines and US
visitors continue to only ever see English on broadcastbeat.com.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switch the bare single-column layouts on these pages to a two-column
grid that pulls in <SidebarAdStack /> on the right (pinned 300x600 +
every 300x250 rotated). Header keeps the leaderboard above. The
in-content 728x90 banners added earlier are removed since the sidebar
now carries the ad load (avoids stacking two of the same unit on the
page).
Pages updated:
- /show-coverage/[slug] event detail
- /about
- /about/contact
- /about/team
- /about/advertise (also gets a real-banner grid in "Available units":
the actual 728x90, 300x600, and 300x250 sample creatives are
rendered next to each unit's description, so advertisers see what
the inventory looks like in production rather than just a label).
The /about pages also got a small refresh: serif H1 + accent-blue
"Broadcast Beat" eyebrow line + bordered content blocks, bringing the
look closer to the cinematic homepage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Footer bottom: replace the plain "Broadcast Beat" text wordmark
with the same logo image used in the top-left header (so footer
matches header brand mark).
- Move the site-wide search input out of the top sticky row and
into the Browse bar that sits below the 728x90 leaderboard. Top
row is now just logo / reading-list / notification bell.
- Browse bar redesign for personality + alignment:
* 48px tall, dark navy gradient with a 2px accent-blue bottom
border and a horizontal sheen across the top edge
* Search input (pill shape, focus-glow ring) sits flush left
* Menu items (Events ▾, Newsletter, Forum, About ▾) are now
right-aligned via flex justify-between
* Each menu item is uppercase + tracked, hover lights an
accent-blue underline that scales in from the left
* EventsDropdown and AboutDropdown buttons now use the same
.bb-browse-item class — fixes the vertical alignment issue
where they were rendering at the old 60px nav-link-bb height
while the new bar is 48px.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- /search and /reading-list already had Header + Footer; add a
single in-content 728x90 banner at the bottom of each main column
(gated on ADS_728X90.length > 0, hidden on mobile).
- /contributor was a bare dashboard with sidebar + main, no site
chrome at all. Wrap it in the standard Header + Footer; drop the
inner sidebar/main wrapper from min-h-screen to a natural-height
flex so the dashboard sits below the header instead of competing
with it for full viewport. Header brings the site-wide 728x90
leaderboard with it, so contributors see the same ad inventory
as the public site without a second in-content banner cluttering
the dashboard panes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
These four landing pages already had the site-wide header leaderboard
via <Header />, but no additional in-content banner. Add one 728x90
slot at the bottom of the main column on each, gated on
ADS_728X90.length > 0 and hidden on mobile to match the header
banner's display rules.
- /gear — category landing
- /technology — category landing
- /forum — community landing
- /authors/[slug] — author profile
Brings these pages in line with /show-coverage and /about/* which got
the same treatment earlier.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/about/contact and /about/advertise were rendering bare <main> tags
with no site chrome at all — no header, no footer, no banners. Both
are now wrapped with the standard
<div className="min-h-screen bg-background"><Header />…<Footer /></div>
shell that the rest of the site uses, which auto-brings the site-wide
728x90 leaderboard with it.
/about/team already had Header+Footer but no in-content banner.
In addition to the header leaderboard, each of the three pages now
also renders one in-content 728x90 banner (gated on
ADS_728X90.length > 0, hidden on mobile to match the header banner's
display rules). This brings the about/* pages in line with the ad
inventory of /show-coverage and the other category landing pages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both /show-coverage and /show-coverage/[slug] were rendering bare
<main> elements with no site chrome — no top nav, no leaderboard ad
above the page, no footer, no in-content banners.
- Wrap each page in <div className="min-h-screen bg-background">
<Header />…<Footer /></div>. <Header /> already brings the site-wide
728x90 leaderboard with it (see Header.tsx footer of the component)
so that banner appears automatically above the page content.
- Add one in-content 728x90 banner per page (between Upcoming/Past on
the index, and between event header / Coverage list on the detail
page) so the show-coverage pages carry the same ad inventory as
other sections.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The DoubleTicker commit (a52f8ad, since reverted) had bundled the
.bb-browse-bar / .bb-browse-row / .bb-browse-item CSS rules together
with the ticker CSS. Reverting the ticker removed the Browse bar
styles too, but Header.tsx still uses those classes for the
below-the-leaderboard nav. Re-add just the Browse bar selectors so
that nav renders correctly post-revert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three connected fixes:
1) Stop the unwanted "translate this page to Portuguese?" popup that
some US users were seeing. Chrome's content-language detector was
mis-firing on imported BB articles that mention Portuguese/Spanish
company/person names. Add to layout <head>:
<meta name="google" content="notranslate" />
<meta http-equiv="Content-Language" content="en" />
<meta name="robots" content="index, follow" />
Together with the existing <html lang="en"> this is the standard
"this site is English, don't offer to translate" signal.
2) Wire the LanguageSwitcher pulldown to actually do something. It used
to set a cookie + flip <html lang> with no effect on visible
content. Now picking a non-English language redirects the current
URL to Google Translate's hosted proxy (e.g.
https://broadcastbeat-com.translate.goog/<path>?_x_tr_sl=en
&_x_tr_tl=es&_x_tr_hl=en
), which renders the entire page translated server-side by Google
and preserves the user's session as they navigate. English stays
on the canonical broadcastbeat.com host.
3) SEO posture: English is the only indexable version. The Google
Translate proxy lives on a different (Google-owned) domain so US
browsers and Googlebot only ever see the English content. The
alternates.languages hreflang block stays absent — re-add only once
real translated routes exist (Phase D).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The main sticky top row now holds only the logo, Reading List icon,
notification bell, and search box. The Events dropdown, navLinks
(Newsletter, Forum), and About dropdown were lifted out of the top
row and re-rendered in a new "Browse bar" that sits directly below
the header leaderboard banner.
Visual result:
[ top utility bar (account / admin) ]
[ logo 📑🔔🔍 ]
[ 728x90 leaderboard ]
[ Events ▾ Newsletter Forum About ▾ ] ← new Browse bar
[ LIVE WIRE ···· ]
[ RECENT FORUM POSTS ···· ]
The mobile hamburger drawer is unchanged — it already keeps all the
nav items consolidated for small screens.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two horizontal lanes stacked on top of each other:
Lane 1 LIVE WIRE red gradient label, pulsing white dot,
latest 14 published articles, 65s loop
Lane 2 RECENT FORUM POSTS green gradient label, pulsing white dot,
latest 14 forum threads, 82s loop
The two lanes use intentionally different animation durations (65s vs
82s) so they never sync up — feels alive like a real wire room. Both
loops use CSS translateX with a duplicated content track for seamless
infinite scroll. Edges fade via mask-image so items dissolve in/out
rather than appearing on a hard line.
Both lanes pause on hover (the whole row) so users can stop motion
to read or click. Each item is a real <Link>; clicking opens the
article or thread page.
Server component — data is fetched once at request time from
bb.wp_imported_posts and bb.forum_threads, cached at the page's
revalidate boundary. No client JS needed.
Old LiveWireTicker.tsx is removed (orphaned, not referenced elsewhere).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the banner only wrote bb_cookie_consent to localStorage. Many
browsers wipe localStorage in strict-tracking-prevention modes, on
"clear site data on close", and in some private-browsing-adjacent
profiles — so the banner re-appeared on every visit even after the
user clicked Accept or Decline.
Now writes to BOTH a `bb_cookie_consent` cookie (path=/, max-age=365d,
SameSite=Lax) and localStorage. Reads from cookie first, falls back to
localStorage for backward compatibility with prior visitors. Banner
stays hidden for 365 days after a decision is made.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two unrelated tidy-ups, bundled:
- src/components/AskBBAI.tsx: remove the "12,847 indexed" subtitle next
to the Ask BB AI floating-action button (placeholder/mock text from
the original mockup; the real index count isn't wired in).
- src/components/Footer.tsx: the footer-bottom logo was sourcing an
image from img.rocket.new — a third-party CDN unrelated to BB
(artifact from the initial rocket.new export). Replace with a plain
text wordmark Link to /home-page. No third-party image, same UX.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Latest section was showing "View Original »" for imported posts and
opening every click in a new tab, even though the /api/public/posts
endpoint doesn't expose an externalUrl — every link actually went to
the internal /articles/<slug> page. Two changes:
- Link text is now "Read More..." for every article in the feed.
- target="_blank" only applies when article.externalUrl is actually
populated (currently never, but keeps the door open for cross-site
syndication later). Internal /articles/<slug> links now open in the
same tab as the user expects.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dates
- FeaturedBentoFromDb hero + 4-up rail no longer render `ago(wp_published_at)`.
The whole component renders only Featured-category posts (filter is
category ILIKE 'featured'), so dropping the date display globally inside
it satisfies the "Featured posts have no date" requirement.
- /gear and /technology category cards: wrap the existing `{article.date}`
output in `{article.category !== "Featured" && ...}` so only non-Featured
posts continue to show their publish date.
- All other category landing pages, /news/<slug> detail, ArticleFeed, and
LiveWireTicker either don't render a visible article date or only use it
for SEO/structured-data fields (kept).
Trends sidebar
- Remove `BB AI · detected trends` block from the homepage (between
ArticleFeed and NewsletterSignup). Drop the unused import and delete
the orphaned TrendsSidebar.tsx component file. The detect-trends data
fetch and `bb_pulse_dot` mini-widget are gone from the page; nothing
else referenced them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The article-row metadata strip rendered three things side-by-side:
{category} | <SourceBadge source>
For imported WP posts whose `category` column was the placeholder string
"legacy", this produced a meaningless "LEGACY | IMPORTED" row on the
homepage. Now:
- Hide the category text entirely when value is "legacy" (any case);
it falls back to nothing while the AI-derived category propagates
in a separate data pass.
- Drop the vertical pipe separator.
- Remove the yellow "IMPORTED" SourceBadge — the article's category
is the user-relevant signal, not its data-source provenance.
The SourceBadge function is now unused and removed. The source-filter
control in the advanced filters panel is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switches the Featured header row from justify-between to gap-3 + flex-wrap
so a 1px <div className="flex-1 h-px bg-[#2a2a2a]" /> can fill between the
label and the "All featured →" link. Same rhythm as The Latest header in
ArticleFeed.tsx:276-277.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates the forum's <head> SEO/social metadata to match the visible header.
Old "Community Forum" / "broadcast engineering professionals" copy was
leaking into Google + LinkedIn / Twitter Card previews after yesterday's
visible-text rebrand.
- metadata.title, openGraph.title, twitter.title: Community → Industry Forum
- All four description strings broadened to "broadcast, motion picture and
post production professionals worldwide"
- Schema.org WebPage name + description aligned
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- "Community Forum" → "Industry Forum"
- "Connect with broadcast engineering professionals" → "Connect with broadcast, motion picture and post production professionals worldwide"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops <AISuggestedArticles variant="compact"> + its ScrollRevealSection
wrapper from /home-page. Component file stays — still rendered on /news
(NewsPageClient uses variant="full").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mobile readers previously saw zero ads above the bottom of the feed
(header leaderboard is hidden md:block, sidebar stack pushes below the
feed at <lg breakpoints). This adds a 300x250 ad block after every 4th
article in <ArticleFeed>, wrapped in md:hidden so tablet/desktop are
untouched (they already get SidebarAdStack to the right of the feed).
- Ad pool: rotateAll('300x250') from src/lib/ads.ts (DB-backed, falls
back to the 7-ad hardcoded pool)
- Cycles through the pool if the feed is longer than the pool
- Skips the insert after the very last article
- Labelled "Sponsored", divide-y separator inherited from parent
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the bento's old hero+side-rail layout with a full-width cinematic
21:9 hero on top + 4-up vertical-card rail below. Single DB query, single
component. Standalone FeatureStoryHero component removed (was rendered
above the bento; design now lives inside the bento itself).
- FeaturedBentoFromDb.tsx: new hero (gradient overlay, Feature Story badge,
large overlaid title, "Read full story" cue) + 4-card grid below
- home-page/page.tsx: drop <FeatureStoryHero /> import + JSX block
- src/components/FeatureStoryHero.tsx: deleted (composition replaced by
inline JSX in the bento for single-query efficiency)
Accent uses var(--color-text-info, #60a5fa) — the existing blue from the
codebase, matching the rest of the post-Slice-4-revert site.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the four accent-family hex codes site-wide with the teal palette
(keeps the dark theme; only swaps the accent family, not the dark base):
#3b82f6 (accent primary CTA) → #5B7C8D (teal) [839×]
#2563eb (accent-dark / hover) → #4A6473 (darker teal) [44×]
#60a5fa (info link, lighter) → #8FB0C3 (mid teal) [68×]
#1e3a5f (accent-muted bg) → #2F4F5F (navy) [44×]
tailwind.config.js tokens updated to match (accent/accent-dark/accent-muted).
Sweep also covers tailwind.css. 108 files touched. The dark base (#0d0d0d,
#111, #161616, #1a1a1a etc.) is intentionally untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New component src/components/FeatureStoryHero.tsx renders a wide 21:9 hero
of the top-pinned featured article (same query shape as FeaturedBentoFromDb).
Wired into home-page/page.tsx above the bento. Teal accent (#5B7C8D) used
intentionally — Slice 4 will sweep the rest of the site to match.
Bento remains intact and may share its top article with the hero; that's
the magazine "lead story emphasized" treatment, not a bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Header.tsx: bare linkedin/instagram/facebook URLs → /broadcastbeat handles; remove TwitterXIcon from social bar + import (Footer retains TwitterXIcon per "DO NOT OVERRIDE" marker)
- AboutDropdown.tsx: drop "Press kit" item (the page itself stays at /about/press-kit, just delisted from menu)
- SystemStatusBar.tsx: remove "Index online" pulse-dot and "N wire sources" span (keep articles count + events count + build version)
- /about, /about/team, /about/press-kit: wrap in Header+Footer (previously rendered bare <main>)
- /newsletter/page.tsx (new): redirect to /newsletter/archive — Header was 404'ing on /newsletter
- /rss/route.ts (new): dynamic RSS feed reading from legacy-source news section; replaces 404 at /rss
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per Ryan 2026-05-15:
- Removed Vector pass footer (no audience value)
- Removed PHASE 6b comment
- Removed corner brackets + monitoring subtitle + rank prefix
- Top 5 entities now show as Sony +12 / Riedel +8 — entity name
serif + count mono, right-aligned
- Background #F5EFEB (beige; #F5EFFEB had 7 hex chars, interpreted as
#F5EFEB) via new --color-trends-bg token
- Text #2F4F5B (navy) via new --color-trends-text token
- Tokens added to redesign-tokens.css palette section
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the 5-slide FeaturedCarouselServer that lost the Sony hero +
side-rail layout. New FeaturedBentoFromDb pulls every category=Featured
row, sorts featured=true (pinned hero) first then by published date.
- Hero (left, 2 columns): Sony BRC-AM7 + HXC-FZ90 — pinned via featured=true
- Side rail (right): next 4 Featured-category posts (BVM-HX1710, MCRs/NOCs,
Tessera SQ200, Backlight Iconik)
Old carousel kept on disk for future use. Layout matches the prior
FeaturedBento.tsx visual structure (hero + 4 small cards right).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tables (Phase B+):
bb.ad_impressions — every render = a row. is_bot + viewport flags
for filtering. No unique constraints — gross
counts per Ry an spec.
bb.ad_clicks — every /r/[slug] hit = a row.
bb.ad_campaign_clients — schema only; future client reporting.
FK target is bb.banner_creatives (the renderer table from 5/15 banner
refresh). bb.ad_campaigns is the AdOps billing schema and unrelated.
Routes:
GET /r/[slug] — log click, 302 to bb.banner_creatives.click_url
POST /api/track/impression — record impression (slug | campaign_id),
used by client beacon
AdImage rewrite:
- link href is /r/{slug} target=_blank rel=sponsored noopener noreferrer
- sendBeacon on mount fires impression (viewport=false)
- additional sendBeacon on IntersectionObserver ≥0.5 (viewport=true)
- removed direct supabase.from('banner_analytics').insert path
Ad type carries slug now; FALLBACK list + DB mapper both populate it.
/admin/banners:
per-row stats — 24h, 7d, lifetime impressions/clicks/CTR
link to /admin/banners/[id]/analytics
/admin/banners/[id]/analytics (new):
recharts line charts for impressions + clicks (30d), top page paths,
top referrer domains, bot/human toggle.
Tower Products orphan image file removed; banner was not in any active
table or in code (already off rotation).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bb.banner_creatives row also inserted (AdSanity 191740, click_url
https://telycam.com/, dates 2024-05-07 → 2026-12-31, status active). The
creative was already in public/legacy/ads/ from the pre-Phase-B banner
pool — just needed the DB row + FALLBACK entry so SidebarAdStack picks
it up before the DB cache fills.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend:
bb.banner_creatives (new) — image_path, click_url, size, start/end_date,
status, adsanity_source_id. Seeded with 8 banners from broadcas_temp
WP-export DB (post_type=ads): LiveU 728x90, Blackmagic DaVinci Resolve
20 (300x600), Studio Hero, Magewell Pro-Convert, LiveU PAYG, AJA
ColorBox, Zixi, Lectrosonics (all 300x250). Dates and click URLs come
from the AdSanity rows; current creatives downloaded from Wayback
snapshot 20260316123737.
src/lib/ads.ts:
- Reads bb.banner_creatives via anon supabase-js client, gated by
status='active' AND start<=NOW()<=end.
- In-process 5-min TTL cache with stale-while-revalidate. Module load
primes the cache; clients see fallback on first hit, live DB on
subsequent.
- Hardcoded FALLBACK list mirrors the seeded rows so the site keeps
rendering banners if Supabase is unreachable during deploy.
- Public helpers (rotateAll, pickAds, ADS_728X90, ADS_300X250,
FIXED_300X600) remain synchronous so existing client component
callsites work unchanged.
/admin/banners:
Admin-only list + inline editor for every banner_creatives row:
name, click URL, start/end date, status (active|scheduled|paused|
expired). PATCH via /api/admin/banners/[id] — service_role bypass for
the write, user_profiles.role admin/administrator for the gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>