-webkit-font-smoothing: antialiased and -moz-osx-font-smoothing: grayscale
were set on body and the Tailwind 'antialiased' utility was applied on
<body>. Both pair correctly with the prior dark theme (light text on
dark surfaces), but on the new white surface they render Inter + Lora
thinner and harsher — strokes lose weight, edges read as jagged.
Removed from src/styles/tailwind.css:59-60 (body rule) and from the
<body className="... antialiased"> utility in src/app/layout.tsx:143.
OS default smoothing (subpixel on Windows / older macOS, 'auto' on modern)
gives Inter + Lora their intended weight and roundness.
dynamic='force-dynamic' + revalidate=0 on the page weren't enough because
FeaturedBentoFromDb -> pickAds() -> fetch in src/lib/ads.ts:98 sets
next:{revalidate:30}. Next.js takes the lowest positive revalidate from
the render tree and opts the whole route into 30-second ISR (visible in
.next/prerender-manifest.json: '/' initialRevalidateSeconds: 30), masking
the page-level force-dynamic directive.
unstable_noStore() called at the top of the HomePage component body
forces an unambiguous, child-tree-overriding opt-out so the Telycam
hero swap (and any future DB-driven hero change) renders on every
request. lib/ads.ts caching is left alone — its revalidate is fine for
ad rotation; we just stop it from poisoning the page-level cache.
(A) src/components/AISuggestedArticles.tsx :186 + :287 — 'AI Suggested Articles' card titles
used text-[#e0e0e0] on now-white card surfaces, making the title invisible until
group-hover flipped it to #1D4ED8. Both occurrences changed to text-[#0F172A].
(ArticleFeed.tsx:627 and NewsPageClient.tsx:276 were already navy from the
prior contrast-fix deploy and cover both pagination + infinite-scroll variants.)
(B) src/components/AvBeatLogo.tsx :99 — header tagline 'Inside the Future of Pro AV'
was rendered in --brand-text-muted (#475569 slate) on a now-blue nav (#1D4ED8),
near-invisible. Changed to rgba(255,255,255,0.85) so it reads against the blue
while sitting visually subordinate to the wordmark above it.
(C) src/lib/articles/excerpt.ts — cleanExcerpt now strips HTML tags (<p>, <br>, <em>, etc.)
and decodes common entities ( , &, <, >, ", ', ') at
render time, then collapses whitespace before the existing trailing-junk/last-word
logic runs. Display-layer only — stored excerpts are not mutated, matching the
'fix at the DISPLAY layer' instruction.
The 2026 rebrand swept surface hexes from dark to light but left near-white text colors baked into components. Three approved fixes:
- FeaturedBento.tsx:255 — 2x2 grid card titles base color #e0e0e0 -> #0F172A (brand-navy). Hover stays #1D4ED8.
- tailwind.css .nav-link-bb — base color #cccccc -> var(--brand-text). Hover unchanged.
- ArticleFeed.tsx :375 + :385 — date input text #888 -> #475569 (brand-text-muted) and dropped [color-scheme:dark] so Chrome stops rendering the dark date-picker chrome on a now-white input.
Findings #2, #3, #6 in the audit were not touched — Ryan flagged them as unverified.
Next.js sets a long s-maxage on prerendered HTML even for routes flagged force-dynamic; Cloudflare then caches that response and ignores subsequent dynamic renders. Explicit Cache-Control + CDN-Cache-Control no-store on / and /news/* keeps the hero swap visible immediately after a DB change.
- Main nav background → brand-blue #1D4ED8 (sampled from logo emblem).
- Nav link text → white with darker-blue hover bg so contrast holds.
- Article card title fixed from #e0e0e0 to navy #0F172A (was unreadable on the new white surface).
- Industry News card date row removed.
- /news Topics filter chip row removed.
- /news article hover from stark #111 to light-blue #EFF6FF tint matching the brand.
- Tagline 'News & Intelligence for Pro AV, Live Production & Display Tech' renamed to 'Pro AV, Display and Live Production Tech' across Header, RSS feed, newsletter welcome template, root layout title, page title, OG/Twitter tags.
- Homepage page.tsx now has `export const dynamic = 'force-dynamic'` + `revalidate = 0` so the hero (FeaturedBentoFromDb) actually re-renders against the live supabase row (previously the page was being statically prerendered which masked DB-driven hero swaps).
- New AvBeatLogo inline-SVG component (rounded-square emblem with
forward-leaning AV monogram + horizontal beam detail) at
src/components/AvBeatLogo.tsx, three variants (full/wordmark/icon).
- Header.tsx now uses the responsive AV BEAT logo lockup in the
top-left, links to /, full lockup on desktop (emblem+wordmark+tagline),
emblem+wordmark on tablet, emblem-only on mobile.
- Removed DualSpeedTicker (the 'RECENT FORUM POSTS' ticker strip);
the news ticker / glow chrome does not match the new aesthetic and the
forum row was the explicit removal target.
- Tailwind theme + globals.css now expose the AV BEAT 2026 palette as
semantic tokens: --brand-blue (#1D4ED8), --brand-blue-bright (#38BDF8),
--brand-navy (#0F172A), --brand-bg (#F8FAFC), --brand-surface, --brand-border,
--brand-text, --brand-text-muted. Legacy --color-* aliases re-point to the
new tokens so any inline-styled component re-themes for free.
- Site-wide hex sweep migrates 2,769 hardcoded color literals across 144
files from the old dark-broadcast palette to the new tokens (orange
-> blue, dark-brown -> white surface / navy text, cream -> navy).
- Layout body class flipped from 'bb-neon' to 'bg-brand-bg text-brand-text'
so the dark glow chrome no longer leaks through the new light theme.
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>