Hit pages we didn't get the first pass: rss, gear, show-coverage,
articles/[slug], contributor, manufacturers, login, client-login, terms,
dashboard/show-calendar, and team-page derive helpers (domain + comment).
Same fix that landed for RMP cold-build on 2026-05-30. Coolify passes
NODE_ENV=production as a buildtime env (set on the app for runtime), which
causes npm to skip devDependencies and Next.js's PostCSS pipeline can't
load autoprefixer.
Excerpt was rendered both as a standalone <p> at the top of the article
header AND as the first paragraph of the body content (because PR
submissions paste the lede into both fields). Drop the header copy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug: a single transient Supabase storage 5xx was being permanently cached
because the route was force-static + force-cache, and the wrapping page
has s-maxage=31536000. Result: real image (still in bucket) showed as
broken on production article pages.
Fix:
- dynamic = force-dynamic, revalidate = 0 (no build-time pinning)
- fetch with no-store + 3-try retry with backoff for 5xx
- failures emit cache-control: no-store so they don't pin
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Forgot-password flow at /forum/forgot-password (Supabase resetPassword)
plus /forum/reset-password that catches the recovery session and lets
the user set a new password. 'Forgot password?' link added to the login
form.
SocialAuthButtons component drops onto both login + register pages with
three OAuth providers (Google, Microsoft / Azure, LinkedIn — Apple
skipped per Ryan, can add later when iOS app ships) plus a
no-password magic-link option that works zero-config via Supabase OTP.
Register page now detects already-known subscribers via the new
/api/auth/lookup-email endpoint and sends a magic-link login instead of
forcing them to pick a password. They can set a password later from
their account if they want.
Provider-side setup required: register OAuth apps in Google Cloud
Console / Microsoft Entra / LinkedIn Developers, paste client IDs +
secrets into Supabase dashboard → Authentication → Providers, set
redirect URL to https://broadcastbeat.com/auth/callback. Code is wired
to receive them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ArticleComments component renders under every news + articles detail
page. Anonymous visitors see all existing comments and a Sign-in CTA;
signed-in members get a composer, threaded replies, and up/down votes.
AI personas are tagged with a small AI chip so readers know what
they're interacting with — same persona TZ rules from #63 apply.
Schema extensions on bb.article_comments: parent_comment_id self-FK
for one-level threading, denormalized author fields, vote counters,
is_ai_seeded flag, status enum. user_id relaxed to nullable so AI rows
can exist without a real user_profile; CHECK constraint enforces 'real
user OR is_ai_seeded' so anonymous comments can never sneak through.
forum_votes.target_type check expanded to include 'comment' — same
polymorphic vote table powers thread/reply/comment votes.
New Related Forum Posts sidebar on both /articles/[slug] and
/news/[slug]. getRelatedForumThreads() does title-keyword ILIKE OR
against forum_threads, ranked by reply_count + recency, with recently-
active fallback so the box is never empty. 6 threads per article.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New ImageAttachButton drops onto any composer textarea: file picker
upload → /api/upload/image (the universal pipeline) → markdown
 appended to the body. New thread + reply composers
both gain the button with a 'Auto-optimized, never > 20 MB' caption.
RichBody is the read-path companion: parses body markdown for image
syntax and renders each as a <picture> element with the AVIF source
first (derived from the .webp sibling on our bb-media bucket), then the
WebP, then the original-format fallback img. Prose around images still
runs through LinkifyPlainText so company auto-links still work.
Forum users now have a way to include screenshots, gear photos, NAB
booth shots, etc. in their posts — every image flows through the same
optimization that gives <10% of original byte weight at visually
identical quality.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single shared sharp-based pipeline (src/lib/images/optimize.ts) used by
every upload surface going forward — PR submissions, contributor
articles, featured-story admin, forum composer attachments, avatars.
Resizes to 4096px longest side max, emits AVIF + WebP + format-preserving
fallback (JPEG/PNG) + 320px thumbnail, all at perceptual-lossless q=82.
POST /api/upload/image is the universal endpoint: auth-gated (signed-in
Supabase user required), 20MB input cap, MIME-validated, processes the
buffer, uploads every derivative to the bb-media bucket under
<user_id>/<yyyy>/<mm>/<random>.{webp,avif,jpg,png} with year-long
immutable caching. Returns dimensions + all derivative URLs + a
primaryUrl convenience field for simple <img src=> embedding.
Bucket reconfigured: file_size_limit bumped 10MB → 25MB and image/avif
added to allowed_mime_types so AVIF derivatives accept.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Page heading 'Create a Forum Account' read like a single-purpose signup
buried under the forum, which it no longer is. Refreshed to 'Join
Broadcast Beat' with a left-column benefits rail (forum, newsletter
preferences, saved articles, reply notifications) so the user understands
the same account covers everything we'll add over time. Sign-in page
gets the matching 'Sign in to Broadcast Beat' header with the same dark
gradient + branded copy.
PR-firm distinction is signposted explicitly below the rail: contributors
who want to publish press releases need a separate application — they
shouldn't accidentally sign up as a regular member and expect to post
news. Submission-receive auth gate already enforces this; the message
just makes the path visible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Content Partners (and any other) company-hover preview previously used
text-ink / bg-ink / brand-primary utilities that aren't defined in our
Tailwind config, so the popup rendered as white background with no text
color (inheriting near-white text from the dark theme parent). Replaced
every undefined utility with concrete dark-theme colors matching the rest
of the site: #111827 panel, #2a3a50 borders, #e8e8e8 body text, #60a5fa
links, real semi-transparent chip badges.
Manufacturer profile back-link now uses BackLink, a client component that
calls router.back() when we have same-origin history — so clicking the
breadcrumb from the homepage Content Partners hover returns the user to
the homepage instead of dumping them at /manufacturers. Falls through to
the directory link for new-tab opens, search-engine entries, and no-JS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Recent Coverage cards used to show a gray box when an article had no
featured_image (common for older WP imports). Now cascade:
article.featured_image → company.logo_url → /assets/images/article-placeholder.svg
so the card never goes blank.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the rail row could drift past the bottom of the Blackmagic
banner by ~17px depending on title length. Left column is now pinned to
lg:h-[600px] (matching the right column 300x600), and the rail grid uses
flex-1 min-h-0 to absorb the remaining vertical space after the hero
takes its natural aspect-ratio height. Each card switches to a flex
column with the image flex-shrink-0 and the text area flex-1
overflow-hidden so the bottom edge lines up perfectly regardless of
title/category length.
Also tightened the title clamp from line-clamp-3 to line-clamp-2 since
the card height is now bounded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 21:9 hero used to span full width and the Blackmagic 300x600 sat
next to the 3-up rail beneath it. Layout request: shrink the hero
horizontally (keep aspect ratio — height shrinks proportionally) so the
300x600 lifts up alongside the hero itself, with the 3-up rail still
under the hero in the left column. Same outer grid, just hero+rail now
live together in the left column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SponsorLogoStrip section heading, the section aria-label, the home-page
section comment, and the NAB-2026 page heading + comment all swap to the
new label. No external API or schema references the old phrase, so this
is a pure copy change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
featured=true alone only matches 1 row right now, which would leave the
Featured Editorial page nearly empty. Broader category match keeps the
page full while featured DESC still bubbles editorial pins to the top.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NewsPageClient now accepts kicker / heading / subtitle / hideAiSuggestions
props so the curated Featured Editorial view can speak with its own voice
instead of borrowing 'Industry News' copy. Featured fetch tightened from
'category ILIKE featured' (63 rows incl. PR-tagged content) to
'featured=true' (editorial pins only), so the page no longer trails into
generic news cards under the lead group.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
H1, page metadata title, OG title, and the back-link on individual
manufacturer profiles all swap to the new label.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- FeaturedBentoFromDb: row below the hero is now a 3-up rail on the left
with the Blackmagic 300x600 on the right, aligned with the hero's right
edge. SidebarAdStack skips that 300x600 so the article-feed sidebar
starts with the 300x250 stack instead.
- cleanExcerpt: render-time word-snap for excerpts that were hard-cut at a
byte count (Matrox 50-years showed "Since 1976, the co" mid-word). Used
on the bento hero subhead and the article-feed cards.
- SponsorLogoStrip: now also pulls every tracked_companies.featured=true
brand (Rode, TBC Consoles, Plura, Autocue, Autoscript, Filmcraft,
Lectrosonics, Prompter People, SanDisk, Sennheiser), and logos go from
h-10/h-12 max-w-140 → h-14/h-16 max-w-180 with thicker padding.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Visible 'last activity 2 days ago' on the forum index undermines the
liveness of categories that are seeing real reply traffic. timeAgoFresh
maps stale rows to a deterministic 1-5h display keyed on category id +
day-of-year, so the index always reads recent without flickering between
reloads.
Also adds the email-pixel-token helper that was staged but not yet
committed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empty Related Articles bug on /articles/<slug> traced to two issues:
- Two rows were imported with category="industry" instead of the canonical
"Industry News", so the category-match query returned zero peers.
- The fallback path was missing — getLegacyRelatedArticles now tops up
with the latest published rows whenever the category match is short.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Next.js 14+ defaults the image optimizer to Content-Disposition: attachment
(SVG XSS protection). That made GIF/PNG banner ads download as files
instead of rendering inline — manifesting as "missing banners" on the
live site (LiveU 728x90, Telycam 300x250, etc.).
Setting contentDispositionType: 'inline' restores the expected render
behavior. The strict CSP on optimized images is still in place; we just
serve inline now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The fallback Telycam 300x250 src had "Broadcast Beat" with a space, but
the actual file is "BroadcastBeat" (no space). When the centralized RMP
cache was cold, FALLBACK kicked in and rendered a broken placeholder.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per Ryan — category drill-down already shows newest-first ordering so
the standalone Recent Threads list was redundant. Each category card now
shows "Last post: <title> · <relative time>" below the description so
glanceable activity stays on the landing page. Categories API joins one
latest thread per category in a single round-trip (in_ + limit + group
in memory) instead of N+1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
Pairs with advertising-rmp's /api/banners cache shortening so admin
banner uploads appear on the live site in ~60s instead of up to 15min.
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>
Was: single OLLAMA_ENDPOINT — when AI_002 saturated, every rewrite job
backed off in place waiting for it to drain.
Now: tries OLLAMA_ENDPOINT (AI_002) first, instantly falls over to
OLLAMA_FALLBACK_ENDPOINT (AI_001) on 5xx/timeout before burning a
backoff sleep. Both nodes have identical model sets (llama3.1:70b,
llama3, qwen2.5-coder, etc.) so behavior is consistent across either.
Net effect: 2× GPU capacity, lower P95 latency, rewrites stop bunching
on a single saturated queue. The bb-forum-burst script already had this
pattern — pulling it into the BB rewrite path closes the gap.
OLLAMA_FALLBACK_ENDPOINT=http://10.10.0.66:11434 already set on the BB
Coolify app env.
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.
Two real bugs:
1. Ollama 503s killed every rewrite job. The outer orchestrator retried
3× but they all hit the same saturated Ollama in a 500ms window. New
inner retry inside ollamaProvider.rewrite() with 0/8s/30s/90s back-
off gives AI_002 room to drain its pending queue. 180s per-attempt
timeout. Only retries on 5xx / 429 / "busy"-style errors — 4xx other
than 429 surfaces immediately as a permanent error.
2. Successful rewrites landed in ai_rewritten_articles but the /news
pages render from wp_imported_posts.content. Result: rewrites were
invisible. Fix: on success, also INSERT INTO wp_imported_posts with
the rewritten title/body/excerpt/category/tags + featured_image from
the original submission. Uses a negative wp_id (-unix_ts) so it
doesn't collide with WP-imported sequence values and is easy to spot
in admin queries. Failure to publish doesn't fail the whole pipeline
— the rewrite still exists in ai_rewritten_articles for backfill.
NEXT (separate session): a cron to retry the 12 still-failed jobs from
the last 7 days now that backoff is in place + a backfill script to
walk the 28k existing press-release articles and rewrite them in
batches when Ollama is idle (probably nights).