- page.tsx fetches bb.events (confirmed + tentative) and passes shows
list into DirectoryClient alongside the tracked_companies rows
- DirectoryClient renders an "All shows" + "NAB+IBC" + one chip per
event in bb.events. Shows attendee count when known; tags "soon" on
shows without exhibitor mappings yet (ANGA COM, MPTS, BroadcastAsia,
SET Expo, NAB NY, SATIS, Hamburg Open, CABSAT, etc.)
- Filter logic uses tracked_companies.shows_attending text[] (added in
migration 0008) instead of the legacy exhibits_nab / exhibits_ibc
booleans — scales to N shows.
The directory page was previously capped at 1,000 rows (PostgREST
default max_rows). Server-side bumped to 5000 in a paired supabase
env update — directory now renders all 1,366 manufacturers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
422 response now reads "Your submission contains disallowed phrase X.
Please remove it and try again. Questions? Email
support@relevantmediaproperties.com." so PR firms know exactly what
tripped the filter without having to ask.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New ObfuscatedEmail client component splits user + domain into two data
attributes server-side, then assembles the mailto on mount. Pre-JS DOM
shows "[email protected]" placeholder; only after client-side hydration
does the real address render. Most simple email scrapers won't execute
JS or follow the data-u/data-d pattern, so addresses stay protected
while remaining one click away for humans.
Editorial persona emails derive from slug (firstname-lastname@broadcastbeat.com).
Ops persona emails (Chloe, Riley) come from pulsedesk.personas.email.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Even though distribute-rmp gates the submitter, re-validate the
contributor's distribute.users.status here before inserting into
bb.native_articles. A leaked DISTRIBUTE_SHARED_SECRET shouldn't be
enough to bypass the approval system. Falls back to letting the
request through on transient lookup failure (distribute side already
checked).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls from pulsedesk.personas (ops staff) and renders them as a second
section below the editorial team. Shows role, email, phone + ext.
Filtered to the company-wide RMP roles only (Chloe Delgado / Accounting,
Riley Park / Support) — not property-specific personas. Avatars fall
back to initials block until proper photos are added.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two compounding bugs caused 0 clicks since May 22:
1. lib/ads.ts exported ADS_728X90 / ADS_300X250 / FIXED_300X600 as
IIFE-evaluated module constants. They captured the FALLBACK array
at module-load time, before the centralized RMP banner cache
populates. Every render that referenced them got FALLBACK banners
whose click_url was the direct advertiser URL — bypassing the
centralized click logger entirely. 300x250s escaped because every
page reads them via rotateAll() at render time.
2. Even when the new RMP-cache banners did render, click_url pointed
at www.RMP.com/<property>/<slug>, which logs to adv.banner_clicks.
The RMP analytics dashboard reads bb.ad_clicks. Repointed click
URLs at advertising.RMP.com/r/<campaign_id> so all clicks land in
the single table the dashboard queries.
Files: lib/ads.ts (remove broken IIFE exports + repoint click_url),
SidebarAdStack, Header, Footer, and 9 pages (reading-list, forum,
forum/thread, search, gear, technology, authors, show-coverage x2).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Standardize "NAB Show" / "2026 NAB Show" across badges, filter chips,
and headings; never bare "NAB" in user-facing copy.
- /manufacturers/[slug]: normalize show name at render, strip
randomstring suffix on booth, hide booth unless the show is within 90
days, filter single-letter junk out of category display.
- News filter "NAB Show" chip loose-matches any nab-tagged article so
legacy "NAB 2026" tags still surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. Clean image URLs
Article HTML was leaking the full Supabase Storage URL:
https://supabase.onsethost.com/storage/v1/object/public/distribute-media/<uuid>/<file>
Now rewritten on render to:
/img/distribute-media/<uuid>/<file>
Served by new /img/[...path] route handler that proxies to Supabase
with 24h public cache + 7d SWR. Better SEO (first-party images) and
stops leaking the storage host into article markup.
2. Stuck PR submissions
When the rewrite pipeline fails (Ollama 503 / bad model output), the
submission used to sit forever in ai_original_submissions without
becoming a public wp_imported_posts row. PR firms got "delivered"
status with no visible article anywhere.
/api/ai/submit now calls publishRawFallback() if rewriteSubmission
returns anything other than 'succeeded'. The fallback inserts a
wp_imported_posts row containing the raw HTML body with a visible
"Distributed as a press release — staff editorial rewrite pending"
banner at the top, so the article surfaces immediately. A future
retry job can swap in the rewritten content when Ollama recovers.
- /manufacturers footer "Contact our editors" linked to /contact which
doesn't exist → 404. Swapped to mailto:editors@broadcastbeat.com.
- hybridAI() was falling through to api.anthropic.com whenever Ollama
failed, which logs were tripping on after my earlier no-paid-AI swap
(Article suggestions error: ANTHROPIC_API_KEY not configured).
Removed both Anthropic call-paths in hybridAI + hybridAIStream.
hybridAI now retries Ollama with 0/3/12s backoff and throws a clear
error after exhaustion — callers (article-suggestions) already have
graceful non-AI fallbacks.
- /manufacturers index + DirectoryClient: convert white/light-ink theme
to BB dark theme so it stops standing out against the rest of the site
- /manufacturers/[slug]: same conversion, includes the new executives
card grid and Headquarters block
- /nab-2026: same conversion, including hero gradient and sponsor tile
borders (amber on dark instead of amber on white)
- Forum: remove "Top Performing Threads" widget from both /forum and
/forum/[slug] (display only — TopThreadsWidget component preserved
in case admins want to re-enable later)
- Forum hero: vertical padding py-10 → py-5, tagline reduced from
three sentences to one. Adds a "Sign in to post" or "Start a thread"
CTA in the hero's top-right depending on auth state.
- Recent Forum Posts ticker: drop replies entirely, surface only thread
titles so breadth of conversation shows instead of the loudest
threads dominating with reply chatter.
Editorial rule is no paid AI services in production — this endpoint was
calling api.anthropic.com which violated that. Rewires to local Ollama
at 10.10.0.67:11434 using llama3:latest (warm, ~4.7GB, reliable).
Adds 3-attempt retry with backoff for 503/busy responses since AI_002
is shared with the article rewrite pipeline that periodically saturates
Ollama's pending queue. Drops max_tokens 1024 → 512 to keep responses
snappy and reduce queue pressure.
OLLAMA_HOST + BB_ASK_AI_MODEL env vars supported for overrides.
Removes the "AI not configured" 503 — the only way that error fires
now is if Ollama itself is unreachable for all 3 retries.
/api/ai/submit and /api/submission-receive now scan incoming payload
title + body against the bb.disallowed_phrases allowlist (cached 3
min) and return 422 with the matching phrases if any are found. Stops
competitor mentions from re-entering the pipeline after the bulk
purge.
Feature 3 — forum auto-linking
Thread + reply bodies in /forum/thread/[id] now wrap company-name
mentions with /manufacturers/<slug> links via a new client-side
LinkifyPlainText component. Loads the directory list once per page
(module-level promise cache), regex-scans each body, preserves
whitespace, caps at 10 links per body to avoid clutter.
CompanyMentionsHover mounted on the page so the same preview card
pops on hover.
Adds /api/public/companies/list — minimal { name, slug } directory
feed, cached 10min.
Feature 4 — /nab-2026 live coverage hub
New page aggregating every directory company with exhibits_nab=true.
Three sections:
- Coverage Partners — active advertisers exhibiting at NAB, larger
tiles with emerald Sponsor border
- Latest from the show floor — last 60 days of articles with NAB
in the title, cinematic grid
- All NAB 2026 exhibitors — alphabetical, capped at 200 with link
to full /manufacturers directory
Every company tile carries data-company-slug for hover previews.
Feature 5b — Sponsor logo strip on homepage
New SponsorLogoStrip server component renders just above the footer
on the homepage, showing logos of every current banner advertiser
with a directory entry + logo. Each links to the manufacturer
profile with the hover-card slug attribute.
Feature 5c — Recent coverage on manufacturer profiles
/manufacturers/[slug] now includes a grid of the 6 most recent
articles matching the company name in the title, with thumbnails.
Renames the existing manufacturer_id-linked block to "Press
releases" since it's a separate data source.
Feature 1 — sidebar
Adds a "Companies in this story" panel on every news article that
surfaces every directory company my linker matched in the body. Each
tile shows logo, name, Sponsor / NAB 26 / IBC badges, and a one-sentence
bio. Tiles link to /manufacturers/<slug> and carry data-company-slug so
the global hover card pops on hover too.
Extends company-mentions.ts with linkifyAndExtractMentions() that
returns both the linked HTML and the ordered list of matched slugs in
one pass, so /news/[slug] doesn't re-scan the body for the sidebar.
Feature 2 — unified typeahead
/api/search/suggest now returns { companies, items } — companies are
matched against bb.tracked_companies (directory_visible=true), capped
at 6, ordered by featured → mention_count, then bumped if they're an
active advertiser on broadcastbeat (adv schema lookup).
Header search dropdown renders a "Companies" section at the top with
logo, name, Sponsor badge (active advertiser) or NAB 26 badge, above
the existing "Stories" results. Each company link carries
data-company-slug so the hover card works in the dropdown too.
When an article body is rendered, scan it for any company name in
bb.tracked_companies (directory_visible=true) and wrap the first
mention of each company in
<a class="company-mention" data-company-slug="...">Name</a>
linking to /manufacturers/<slug>. Caps at 12 mentions per article so
heavy press-release lists don't get visually noisy.
Match rules:
- Word-boundary, case-insensitive
- Skips text already inside <a>, <script>, <style>, code/pre/headings
- Skips any element with data-no-autolink="true"
- Longest name wins on overlap (Sony Pictures beats Sony)
- One link per company per article (first occurrence)
Hover behavior: a new CompanyMentionsHover client component mounts once
per article page, listens globally for [data-company-slug] hovers, and
pops a floating card showing logo, name, 2-sentence bio, sponsor/NAB/IBC
badges, HQ city, 3 recent stories, and links to the full profile +
external website. Preview data fetched from new endpoint
/api/public/companies/[slug]/preview
with a client-side cache keyed by slug.
Includes non-advertisers — any visible directory entry gets linked,
which is the point. If a company is *also* a current banner advertiser
on broadcastbeat, the hover card adds a green "Sponsor" badge.
Styling: dotted underline that becomes solid on hover, slightly bolder
weight, accent color on hover. Subtle so multi-mention paragraphs don't
look like a link-soup.
Three-tier sort on the Industry News feed:
1. Premium — articles pinned via adv.featured_story_slots (paid slots).
Sorted by position ascending. Highlighted with an amber left-border
gradient + ★ Featured ribbon.
2. Advertiser — articles whose title contains the company name of a
current active banner advertiser on this property, AND were published
in the last 48 hours. Highlighted with a blue left-border gradient
+ ◆ Advertiser ribbon. Multiple matches ordered by publish date.
3. General — everything else, newest first.
After 48h, advertiser stories naturally drop back into the general
tier. Premium slots fall off when their ends_at passes.
Detection is title-only with case-insensitive word-boundary match; the
longest matching company name wins so "Sony Pictures" beats "Sony"
when both advertise. Backed by the new adv.active_advertisers_by_
property view.
createAdminClient() now accepts an optional schema arg so we can hit
adv from BB without juggling clients.
API revalidate dropped 300 → 60s so the feed reflects campaign flips
within a minute.
Renders bb.manufacturer_executives rows as a card grid (photo, name,
title, bio excerpt, LinkedIn link). Adds a Headquarters section with
the street-level hq_address + phone (tel: link) + contact email. Bio
text is now whitespace-preserving so multi-paragraph copy from the
admin editor renders correctly.
Was linking to /news?category=Featured which silently fell back to the
unfiltered /news listing because "Featured" isn't in CATEGORIES and the
filter is tag-based, not category-column-based.
Adds getLegacyFeaturedArticles() that mirrors FeaturedBento's query
(category ILIKE 'featured', featured-flag-first sort) at limit 200, and
a thin /news/featured route that reuses NewsPageClient.
Problem: /admin/* pages redirect unauthed users to /login or /account,
both of which then hit middleware redirecting to distribute.relevant
mediaproperties.com. Distribute is on a different domain, so its session
cookie doesn't carry to broadcastbeat.com — sign-in there left admins
stranded on distribute's dashboard.
Fix:
- Remove /login (and /register) from middleware REDIRECT_PATHS. wp-login,
wp-admin, client-login still bounce to distribute.
- /login is now a real BB sign-in form (was a redirect to /client-login).
- Admin pages preserve return path via /login?next=<encoded path>.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New public routes:
/manufacturers - searchable index, filter by show + first letter
/manufacturers/[slug] - profile page with bio, categories, trade shows, news, JSON-LD
Data sourced from bb.tracked_companies + bb.trade_show_exhibitors,
seeded from the NAB Show 2026 Map Your Show exhibitor list.
Adds schema extensions on bb.tracked_companies (slug, logo_url, bio,
contact, social handles, press_url, ad_status, exhibits_nab/ibc) and
bb.press_releases (manufacturer_id FK, source_url, published_at, status).
Each profile page emits Organization JSON-LD for SEO + canonical URL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Homepage / Industry News query filters wp_imported_posts to
ai_rewritten_at IS NOT NULL so raw press releases never surface on
the editorial feed (NewsArticle JSON-LD was already in place).
- New /news-sitemap.xml route emits a Google News sitemap covering
rewritten editorial published in the last 48 hours, with
news:publication + news:publication_date + news:title + keywords.
- robots.ts now points crawlers at both sitemap.xml and
news-sitemap.xml.
After deploy, submit broadcastbeat.com to Google News Publisher Center
to start serving in News.
gear, technology, search, reading-list, show-coverage, authors/[slug],
forum/thread/[id] all now render the 300x600 + 300x250 stack as a
floating right column at the top of the content area. Existing
single-column page structure preserved (content wraps).
Maximizes impression count: every pageview across these pages now
counts 300x600 + every 300x250 in the stack, on top of the existing
728x90 in Header.
When multiple profiles share a display_name (Michael Carter, Alex
Rivera, etc.), .maybeSingle() blew up with PGRST116, returning 404.
Switched to ordered limit(1) so the earliest-created profile wins
and the route resolves deterministically.
- New DualSpeedTicker component (CSS keyframes, GPU-accelerated,
pause on hover, 28px rows, 17s BEAT / 40s FORUM, refresh 5min)
- /api/ticker/beat-news: last 7 days from bb.native_articles
- /api/ticker/forum-posts: latest threads + replies interleaved
- Mounted at the bottom of Header so every page gets it
- Scoped .bb-neon palette on homepage root: section heads +
text-shadow glow, neon-tinted card borders, emerald→cyan button
gradient, cyan hover state on links, subscribe boxes neon-bordered
- CSS-only — no layout changes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- API: require auth on thread+reply POST (401 if !user); lazy-create
forum_user_profile via service-role; author_name = display_name
- UI: surface API errors (kill silent catch); auto-redirect to login
on 401; gate new-thread + reply forms with "Sign in to post" CTA
- /forum/login + /forum/register: forum-themed Supabase Auth pages
with ?next= redirect support
- /forum/profile/edit + /api/forum/profile/me: edit own forum profile
(username, display_name, bio, role, company, location, avatar)
- Header: logout button + sign-in/join links when not authed
- forum/user/[username]: show Edit Profile button when viewing own
Fixes posting bug: RLS rejected anon inserts with cryptic message
that the UI silently swallowed; now blocked with friendly 401 + redirect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous state: '/' was 307-redirected to '/home-page', and the
home-page route declared canonical='/home-page'. Net effect on
SEO: Google indexed '/home-page' as the canonical homepage and
treated '/' as a transient redirect (no link equity passed).
Changes:
- next.config.mjs: replace the source:'/' destination:'/home-page'
temporary redirect with source:'/home-page' destination:'/'
permanent (301). Any existing crawler entry for /home-page now
consolidates onto '/'.
- src/app/page.tsx (new): re-exports default + metadata from
./home-page/page so '/' serves the homepage directly. No file
moves required, no relative-import breakage in the home-page
component subtree.
- src/app/home-page/page.tsx: canonical and openGraph.url
'/home-page' → '/'. The page still builds at /home-page but
the 301 above will redirect any hit before render.
- src/app/sitemap.ts: drop the duplicate /home-page entry — Google
shouldn't be told about a URL that just redirects.
- layout.tsx: load Lora (serif) + Inter (sans) via next/font/google,
self-hosted at build time, exposed as --font-serif / --font-sans
CSS variables on <html>. No CDN fetch, no FOIT, no GDPR concern.
- tailwind.css: --font-heading and --font-body now reference the
next/font variables with Georgia / -apple-system fallbacks for the
brief window before the self-hosted face is ready. --font-mono
upgraded to IBM Plex Mono → ui-monospace → Courier New fallback
(DoubleTicker chips already specify IBM Plex Mono directly).
- ArticleFeed: drop the FEATURED filter chip (duplicates the
Staff Editorial section above), add publish date alongside the
byline on every card, lift the homepage feed cap from 200 → 5000.
- api/public/posts: lift MAX_LIMIT to 10000 so client-side
pagination + infinite-scroll see the full archive.
1. Remove SystemStatusBar from root layout. The "29,220 articles
indexed / 13 events tracked / [v2.0.0]" strip ate vertical space
above the fold without serving readers.
2. /api/newsletter/subscribe was returning 500 "Failed to save
subscription" because:
- The route used the anon-session client and tried to UPSERT on
bb.newsletter_subscribers, but the table's RLS only has an
INSERT policy for the public role. Upsert needs INSERT+UPDATE,
so the second branch failed RLS.
- Switched to createAdminClient (service-role) — bypasses RLS,
which is fine because this is a server-side endpoint with
server-side email validation.
- Welcome email now sends via nodemailer in a non-blocking
background task so a slow SMTP doesn't hold up the response.
Newer HTML template, brand-aligned, with a real CTA back to
/news.
Side work to wire SMTP (done out-of-band):
- Created newsletter@broadcastbeat.com mailbox in Mailcow via
the API (2GB quota, active, no force-pw-update).
- Set SMTP_HOST=mail.onsethost.com, SMTP_PORT=587,
SMTP_USER/PASS/FROM_EMAIL/FROM_NAME in Coolify env so the
welcome email actually fires after the next deploy.
3. FeaturedBentoFromDb section header: "Featured · staff editorial"
(small mono caps) → "Staff Editorial" (font-serif 2xl/3xl, bold,
with a thin metadata line "Selected by our newsroom" beside it
and a 2px bottom rule). Matches the editorial typography on the
article pages.
Renders on every page via root layout. Behavior:
- 5s delay after first paint so it doesn't slam the user.
- Animates from a pill into the full card (bb-modal-pop keyframe);
respects prefers-reduced-motion.
- Heading "Get the latest in broadcast & media", email field,
Subscribe + "Not now" + close (X).
- Subscribe POSTs to the existing /api/newsletter/subscribe route
(native system — newsletter_subscribers table + SMTP welcome
via Mailcow/SMTP2GO). Listmonk wasn't installed anywhere on the
infra; switching to it later is a one-line URL change.
- "Not now" + backdrop click + Escape + X all dismiss for 7 days
via localStorage["bb_newsletter_dismissed"].
- On success → "Check your email" panel + auto-close at 4s;
subscribed flag stored in localStorage so the modal never
re-prompts that visitor.
Styling matches the site's dark-navy + electric-blue palette
(NOT the neon ticker palette). No layout/page shifts — overlay
only.
PLACEHOLDER: "Search broadcast news…" → "Search articles, gear,
vendors". Drops the weird ellipsis and the "we're a broadcast site
so 'broadcast news' is redundant" feel.
API (/api/search/suggest):
- Match expanded from {title, author_name} to
{title, excerpt, content, author_name} — surfaces hits when the
phrase lives deep in the article body.
- Returns a snippet object {text, matchStart, matchEnd} sliced
from the first hit (HTML-stripped), with ~80 chars of context on
either side and "…" markers when truncated.
- Returns matched_in: 'title' | 'excerpt' | 'content' | 'author' so
the UI can show "match in article body" / "match in byline".
- Returns date + author_name on each result.
UI dropdown (Header.tsx):
- Each suggestion now renders three rows:
1) Title (bold) + category chip
2) Snippet with the matched substring wrapped in <mark>
3) Author byline + date + (if match wasn't in title) a tag
showing where the match occurred
- Vertical stacking with a colored left-border on hover so the
active suggestion is visible.
CSS:
- Stack item layout, snippet with -webkit-line-clamp:2 so long
matches don't overflow.
- <mark> background uses the accent blue at 28% so the highlight
reads against the dark card.