Commit Graph

269 Commits

Author SHA1 Message Date
Ryan Salazar
39e9777e0a forum: image attachments on threads + replies, AVIF picture render
New ImageAttachButton drops onto any composer textarea: file picker
upload → /api/upload/image (the universal pipeline) → markdown
![alt](primaryUrl) 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>
2026-05-29 16:08:59 +00:00
Ryan Salazar
e24d78cd15 images: unified server-side optimization pipeline + /api/upload/image
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>
2026-05-29 15:43:04 +00:00
Ryan Salazar
976c979e54 forum register/login: rebrand as Broadcast Beat membership
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>
2026-05-29 15:39:02 +00:00
Ryan Salazar
a045d6c558 manufacturer hover popup + smart back link
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>
2026-05-29 15:34:54 +00:00
Ryan Salazar
cbdf61073f manufacturers: never-empty image cascade for coverage cards
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>
2026-05-29 15:28:48 +00:00
Ryan Salazar
dd2a8c8da7 bento: lock left column to 600px so 3-up rail bottom aligns w/ 300x600
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>
2026-05-29 13:31:46 +00:00
Ryan Salazar
cc76c21c24 homepage bento: hero shrinks into 1fr+300px grid w/ banner at top right
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>
2026-05-29 13:21:04 +00:00
Ryan Salazar
6a2d3af336 homepage + NAB: rename 'Coverage Partners' → 'Content Partners' sitewide
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>
2026-05-29 12:16:21 +00:00
Ryan Salazar
bf9e838ea9 news/featured: broaden filter back to category=featured w/ pinned-first sort
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>
2026-05-29 11:07:11 +00:00
Ryan Salazar
07966f556f news/featured: own header + tighter pool, hide AI suggestions sidebar
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>
2026-05-29 11:06:00 +00:00
Ryan Salazar
a93133e71f manufacturers: rename 'Manufacturer Directory' to 'Production Industry Manufacturers'
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>
2026-05-29 10:57:51 +00:00
Ryan Salazar
3b74f610a2 homepage: bento 3-up + 300x600 inline; word-safe excerpt; bigger sponsor logos
- 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>
2026-05-29 10:56:38 +00:00
Ryan Salazar
0541103c89 forum: clamp category card timeAgo to <=5h via deterministic per-cat hash
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>
2026-05-29 10:41:01 +00:00
Ryan Salazar
29862fe394 articles/[slug]: drop Stay Updated sidebar card; related fallback to latest
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>
2026-05-29 10:40:44 +00:00
Ryan Salazar
0a97e6d168 next.config: contentDispositionType inline — fixes banner ads downloading instead of rendering
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>
2026-05-28 22:28:55 +00:00
Ryan Salazar
3b4c04b759 lib/ads: fix Telycam FALLBACK filename (BroadcastBeat, no space)
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>
2026-05-28 22:19:28 +00:00
Ryan Salazar
077f146eda forum: drop Recent Threads widget, surface last post inline on category cards
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>
2026-05-28 22:14:50 +00:00
Ryan Salazar
52d5029eed manufacturers directory: dynamic per-show filter buttons
- 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>
2026-05-28 17:14:13 +00:00
Ryan Salazar
e163746d87 submission-receive: name the disallowed phrase in error + support link
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>
2026-05-28 14:39:37 +00:00
Ryan Salazar
b3ec895c50 lib/ads: drop in-process cache 5min → 30s
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>
2026-05-28 14:31:49 +00:00
Ryan Salazar
28c05962ce team page: publish employee emails with crawler obfuscation
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>
2026-05-28 14:21:44 +00:00
Ryan Salazar
74ab4dae3d submission-receive: defense-in-depth approval check
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>
2026-05-28 14:13:27 +00:00
Ryan Salazar
73b599f8be team page: add Accounts & Support section with Chloe + Riley
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>
2026-05-28 14:11:54 +00:00
Ryan Salazar
ed764b1bbe ads: route all clicks through advertising.RMP.com/r/<id> + cache-aware banner reads
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>
2026-05-28 02:07:06 +00:00
Ryan Salazar
95eba5794b manufacturers + NAB Show naming sweep
- 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>
2026-05-27 22:07:29 +00:00
Ryan Salazar
ec3118e7bc rewrite: dual-endpoint Ollama failover (AI_001 + AI_002)
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.
2026-05-27 19:57:26 +00:00
Ryan Salazar
61910d6c8f fixes: image-url proxy + raw-publish fallback on rewrite failure
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.
2026-05-27 19:43:35 +00:00
Ryan Salazar
6e3a93f90d fixes: dead /contact link + remove paid-AI fallbacks from hybridRouter
- /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.
2026-05-27 18:46:10 +00:00
Ryan Salazar
346868fea3 rewrite-pipeline: Ollama backoff + auto-publish to wp_imported_posts
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).
2026-05-27 17:38:59 +00:00
Ryan Salazar
0d542fabf8 header: move secondary bar (tagline / socials / sign-in / subscribe) under main nav
Was: top bar → main nav (sticky) → browse bar
Now: main nav (sticky) → secondary bar → hairline → browse bar

Result: the tagline / language flag / sign-in / join / email subscribe
row no longer eats vertical space above the logo + main nav. It scrolls
off naturally once the user starts reading (main nav stays sticky).
Hairline `h-px bg-[#2a2a2a]` separator below mirrors the divider
pattern used next to "Staff Editorial" elsewhere on the site.

Identical contents — the entire block was relocated, no behavior or
text changed.
2026-05-27 17:22:19 +00:00
Ryan Salazar
dc29ea1442 redirect /about/advertise → RMP broadcast-beat media kit (301) 2026-05-27 14:55:34 +00:00
Ryan Salazar
5ab27d7912 nav: remove NAB 2026 menu entry 2026-05-27 14:53:08 +00:00
Ryan Salazar
3460baa8b9 ux: dark-theme conversion + forum hero + ticker cleanup
- /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.
2026-05-27 14:52:08 +00:00
Ryan Salazar
52a6792c9e ask-bb-ai: swap from Anthropic to local Ollama on AI_002
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.
2026-05-27 13:39:24 +00:00
Ryan Salazar
1793c4f0e2 ingestion: reject submissions containing disallowed phrases
/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.
2026-05-27 13:34:41 +00:00
Ryan Salazar
95a230b1ef articles: strip dead WP image tags from imported post bodies
rewriteLegacyImageUrlsInHtml only swapped URLs that had a mapping in
legacy-image-map.json — anything missing the map was left pointing at
the dead https://www.broadcastbeat.com/wp-content/uploads/ host (404
in browsers, bad for SEO, broken-image icons everywhere).

New stripDeadLegacyImages() pass runs after the rewrite and removes:
- <figure> blocks whose only inner image is a dead WP image
- <a> wrappers around dead images
- Bare <img dead> tags
- Empty <figure> leftovers
…leaving body copy intact and figcaption-less.

cleanLegacyImages() is the one-call helper that pipes both steps.
Applied via rowToArticle so every /news/<slug> render gets it.
2026-05-27 13:20:42 +00:00
Ryan Salazar
eb9029e311 nav: add Directory + NAB 2026 to main menu 2026-05-27 13:15:23 +00:00
Ryan Salazar
2e78726291 cross-link: forum auto-link, NAB hub, sponsor strip, recent stories
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.
2026-05-27 12:55:25 +00:00
Ryan Salazar
a59846a524 cross-link: companies-in-this-story sidebar + unified company search
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.
2026-05-27 12:47:54 +00:00
Ryan Salazar
621d7f45fc deps: add cheerio for company-mentions article-body linker 2026-05-27 12:38:24 +00:00
Ryan Salazar
b479b62331 articles: auto-link company mentions to manufacturer directory
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.
2026-05-27 12:32:14 +00:00
Ryan Salazar
c1ab36297c homepage: auto-promote advertiser stories + premium slots in Industry News
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.
2026-05-27 12:25:20 +00:00
Ryan Salazar
d82b8edd21 manufacturers/[slug]: executive leadership + phone + street address
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.
2026-05-26 23:45:04 +00:00
Ryan Salazar
8c4721bcbd homepage: All featured → goes to dedicated /news/featured page
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.
2026-05-26 19:24:18 +00:00
Ryan Salazar
4ec3e48c62 fix: drop next.config /login → /client-login redirect
next.config redirects fire before middleware/page.tsx, so /login still
307'd to /client-login (which bounces to distribute). Removing the
config rule lets src/app/login/page.tsx serve a real BB-direct sign-in
form for admin access.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:08:58 +00:00
Ryan Salazar
17e8d9914e fix: BB admin login no longer bounces to distribute
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>
2026-05-26 14:28:29 +00:00
Ryan Salazar
0e733ca0f8 manufacturers: public directory with 1,142 NAB 2026 exhibitors
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>
2026-05-26 14:18:16 +00:00
Ryan Salazar
107840c5f3 google news phase 1
- 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.
2026-05-25 17:03:19 +00:00
Ryan Salazar
09fada759e ads: remove duplicate 728x90 from author page (Footer banner remains) 2026-05-25 16:58:57 +00:00
Ryan Salazar
78d992fda7 ads: server-side impression beacon in middleware (AdSanity parity)
Fires one impression per page render, per slot, server-side — mirrors
AdSanity's WP-level counting model. Picks one banner from each slot's
rotation pool per request. Edge-worker-cached pool refresh, 5min TTL.

Closes the structural gap with historical AdSanity numbers
(client-side counting was dropping 25-40% to ad blockers, plus the
beacon failed silently on fast navigations).
2026-05-24 23:26:02 +00:00