Commit Graph

266 Commits

Author SHA1 Message Date
Ryan Salazar
78c2ee656e homepage: typography upgrade + ArticleFeed polish
- 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.
2026-05-21 01:51:29 +00:00
Ryan Salazar
567a45079f homepage: shrink Staff Editorial header to match The Latest
Earlier this evening I bumped the Staff Editorial header to a
font-serif 2xl/3xl bold — too loud, fought with The Latest section
header below it on the same page.

Reverted to the same shape The Latest uses:
  <span class="section-label">Staff Editorial</span>
  <div class="flex-1 h-px bg-[#2a2a2a]" />
  <Link>All featured →</Link>

That's the global .section-label utility (font-body 13px,
font-weight:700, letter-spacing:0.08em, uppercase, color
accent-blue, with a 3px accent border on the left). Now the two
section headers on the homepage are visually consistent.
2026-05-20 21:52:53 +00:00
Ryan Salazar
74677a80b8 homepage: remove top bar + fix subscribe + Staff Editorial header
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.
2026-05-20 18:51:25 +00:00
Ryan Salazar
8f8eaa5fe8 newsletter: site-wide signup modal with 7-day dismiss
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.
2026-05-20 17:55:29 +00:00
Ryan Salazar
418027b469 search: real autocomplete previews + clearer placeholder
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.
2026-05-20 16:22:04 +00:00
Ryan Salazar
c1705e1a4b ai: bump hybridRouter connection timeout 8s → 30s
Manual fire of bb-company-coverage-scan tonight failed because the
8s connect timeout was tripping on llama3.1:70b cold-load when
another model was resident in VRAM on AI_001. Bumped to 30s so
the orchestrator can wait through a model swap.

Also bumped background request timeout 120s → 180s for the same
reason — story generation against the 70B can take >2 min on a
saturated GPU.

Coolify env OLLAMA_ENDPOINT was simultaneously flipped from
AI_001 (10.10.0.66) to AI_002 (10.10.0.67) since AI_002 is less
loaded — AI_001 is doing author enrichment and scrubber work.
2026-05-20 13:32:09 +00:00
Ryan Salazar
78e08046a7 ticker: redesign — news-wire aesthetic, drop the bright label box
The previous version used solid neon-green / neon-cyan blocks for
the "Live Wire" / "On Calendar" labels which read as too aggressive
against the otherwise dark-navy site. Replaced with a real news-wire
look:

- Pulse-dot indicator (animated breathing dot) + small-caps mono
  label on the left, separated from the feed by a thin rule.
- Item codes (SONY / MTRX / NAB) become small bordered chips with
  subtle neon background tint instead of bare colored text.
- Timestamps are mid-gray monospace; titles are clean Inter at
  13px with slight letter-spacing.
- Top + bottom of each bar get a hairline gradient (neon→cyan
  on row 1, dimmer cyan-only between rows) instead of a hard
  border.
- Items are separated by faint vertical hairlines, padded
  consistently.
- Marquee slowed slightly (110s / 170s) so titles are legible.

Same data sources (news + events), same scroll directions, just
better visual integration with the rest of the dark-navy site.
2026-05-20 13:13:25 +00:00
Ryan Salazar
097181eb43 forum: render threads on /forum default view (not just search mode)
Bug: the index page only fetched threads when user was actively
searching, filtering by category, or sorting non-default. Default
load → setThreads([]) → forum looked empty even though 50+
threads existed in the DB.

Fix: always fetch on mount + filter changes (drop the
isSearchMode gate on the effect), and add a Recent Threads
section in the default view above the Browse Categories list,
showing the 12 most recent threads with author + reply count.

API + DB unchanged.
2026-05-20 13:11:32 +00:00
Ryan Salazar
f6b52f7c2d forum: user profile pages + clickable bylines + neon pulse favicon
USER PROFILES (/forum/user/[username]):
- New API at /api/forum/user-by-username/[username] resolves by
  username OR display_name (URL-encoded), since forum threads/replies
  store author_name = display_name. Returns the persona's profile,
  thread history (up to 50), and recent replies (up to 20).
- New page renders the persona card with archetype badge
  (Audio Engineer / Colorist / etc.) + expertise level badge
  (beginner/intermediate/senior) + role title, company, location,
  years_experience, join date, and an expertise-tags chip row.
- Two columns underneath: "Threads started" and "Recent replies"
  with proper deep-links into each thread.
- Author names in /forum, /forum/[slug], and /forum/thread/[id] are
  now <Link> elements pointing to /forum/user/<displayname>, with
  onClick=stopPropagation so the card-level click handler doesn't
  also fire.

FAVICON:
- Hand-built neon pulse waveform — emerald→cyan→purple gradient on
  dark navy background, designed to read at 16×16 to 256×256.
- Multi-resolution favicon.ico (16/32/48/64) + SVG + PNGs at
  16/32/48/64/96/192/256 under /static/favicons/.
- layout.tsx icons block now references all sizes including the
  apple-touch sizes (192, 256).

No env / DB changes here — pure deploy.
2026-05-20 12:56:56 +00:00
Ryan Salazar
dc166dfb10 forum: fix API + UI column-name mismatch against bb.forum_threads
The schema has upvotes / downvotes / vote_score; API + UI were
querying/reading upvote_count / downvote_count (non-existent
columns). Threads existed in the DB but didn't render.

- /api/forum/threads route.ts:
  - 'top-voted' sort: order by vote_score (was upvote_count)
  - 'newest' default sort: forum_threads has no 'status' column,
    so order pinned-first via is_pinned, then last_reply_at desc
- UI ForumThread interface + components: rename
  upvote_count → upvotes, add downvotes + vote_score so the
  vote display in the thread cards and the admin moderation
  list pulls live data from the existing columns.

After this lands and the deploy completes, the 50 seeded
threads + the 9 ai-seeded new threads + 253 backfilled replies
already in the DB will all render at /forum.
2026-05-20 08:34:05 +00:00
Ryan Salazar
4e11438963 seo: open site to search engines + expand sitemap to full catalog
- layout.tsx: flip robots.index from false→true (and the matching
  googleBot block). Drop the manual <meta name="robots"
  content="index, follow"> in the JSX head — it was duplicating
  (and conflicting with) the metadata-driven tag, so Google was
  seeing both "index, follow" AND "noindex, nofollow" and taking
  the more restrictive one. Now the metadata block is the single
  source of truth.
- sitemap.ts: expand from 5,000 articles to up to 45,000 (catalog
  is ~26k; cap leaves room for static + authors + events under
  Google's 50k-per-file ceiling). Add /authors/[slug] entries
  pulled from bb.author_profiles, and /events/[slug] entries
  pulled from bb.events. Drop the duplicate /articles/[slug]
  emissions — /news/[slug] is the canonical news URL.
- legacy-source.ts: page through the supabase query in 1,000-row
  chunks via Range headers so the 25k cap actually returns the
  full catalog (the previous `.limit(25000)` was getting silently
  truncated at PostgREST's default 1,000 max_rows).

Phase B (Ollama rewriter) is live; the press-release scrubber +
AI featured-image jobs are running in background to clean the
catalog. User explicitly chose to open SEO now rather than wait
for those jobs to finish.
2026-05-20 07:17:59 +00:00
Ryan Salazar
a9823b1d8a ticker: replace LiveWire with dual-row neon ticker (news + events)
Row 1 (Live Wire, scrolls →): most recent imported + AI-rewritten
articles, neon-green label, monospaced [HH:MM] [SOURCE_CODE] prefix.
Row 2 (On Calendar, scrolls ←, slower): upcoming events from
bb.events (confirmed/tentative, start_date >= today), neon-cyan
label, [Mon DD] [SHORT_NAME] prefix.

Defensive: each query is in its own try/catch so a partial failure
renders an empty row rather than crashing the page — that was the
silent build crash that took down the prior DoubleTicker incarnation.
If BOTH queries return zero rows we render nothing instead of two
blank bars.

CSS tokens added to redesign-tokens.css:
- bb-marquee--reverse + bb-marquee--slow modifiers
- .bb-neon-bar / .bb-neon-label / .bb-neon-time / .bb-neon-code
  (scoped class names so this is the ONLY place on the site that
  uses the neon palette — rest of the site keeps dark-navy + blue)

User cancelled the wholesale neon redesign; only the dual-ticker
keeps the neon green/cyan styling.
2026-05-20 07:02:15 +00:00
Ryan Salazar
5c3432915a preview: drop neon redesign sample at /previews/neon.html 2026-05-20 06:57:01 +00:00
Ryan Salazar
80b307ba92 ai: implement ollama rewrite provider + ryan-salazar byline default
The press-release rewriter (ai_rewrite_jobs) was stuck because:
- BB_REWRITE_PROVIDER defaulted to "anthropic" in code, AND
- production env still had BB_REWRITE_PROVIDER=anthropic, AND
- ollamaProvider.rewrite() was a stub that threw "not implemented".
Every job since 2026-01-26 hit Anthropic 400 "credit balance too
low" — 12 failed_api in a row.

Changes:
- Implement ollamaProvider against /v1/chat/completions on the local
  Ollama (works against AI_001:11434 / llama3.1:70b). Same JSON
  contract as the Anthropic provider; gets prompt_tokens /
  completion_tokens out of Ollama's usage block.
- Flip the default provider from "anthropic" → "ollama" in
  getProvider(), so the rewriter works out of the box now that the
  Ollama implementation exists.
- Make the BB auto-story byline configurable via env. Default flips
  from a 12-name pen rotation to a single "Ryan Salazar" byline for
  Broadcast Beat (per editorial directive). AV Beat keeps its own
  pen pool. Set BB_AUTOSTORY_BYLINE=rotate to restore the old
  behaviour.

Production env still needs (set in Coolify dashboard):
  OLLAMA_ENDPOINT=http://10.10.0.66:11434
  OLLAMA_MODEL_DEFAULT=llama3.1:70b
  BB_REWRITE_PROVIDER=ollama
  BB_REWRITE_MODEL=llama3.1:70b
  (remove ANTHROPIC_API_KEY)
2026-05-20 06:42:22 +00:00
Ryan Salazar
f7c462891f authors: db-driven /authors/[slug] for all 160 contributors
Drops the 540-line hand-curated map and replaces it with a real
bb.author_profiles table + view (author_profiles_with_stats) that
computes first_post_date / total_articles live from
wp_imported_posts. The page now resolves any slug correctly:

- Slug looked up in author_profiles; if no row, the slug is
  titleized ("desert-moon-communications" → "Desert Moon
  Communications") and used as the wp_imported_posts.author_name
  filter, so articles + join date still resolve.
- "Joined ..." line is now the actual MIN(wp_published_at) for that
  author, formatted as Month YYYY. Fixes Desert Moon showing 2020
  when its first post was Nov 2013.
- Role line defaults to "Contributor"; staff/founder roles ride on
  the new role column. Ryan seeded with "Founder of Broadcast Beat"
  in the migration.
- About tab renders a contact card (address / phone / email /
  website) whenever any of those fields exist on the profile row,
  plus a profile-facts panel with location + member-since +
  articles published + role.
- Bio is from the profile row; when empty, page shows
  "No biography has been added yet for {name}" instead of falling
  back to another author's bio.

Bios + contact details for the remaining ~160 authors are populated
by a separate Ollama+web-fetch enrichment job (next commit).
2026-05-20 06:25:08 +00:00
Ryan Salazar
fa94e92af5 chore: purge all rocket.new references from source
- Replace img.rocket.new mock URLs in authors/[slug] + FeaturedBento
  with /assets/images/article-placeholder.svg (live DB data drives
  these pages now; the static maps are fallback only).
- Replace broadcastb5322.builtwithrocket.new with broadcastbeat.com
  in admin API routes, news detail, forum layout, and the 4 Supabase
  edge functions that build outbound email links.
- Swap rocket-hosted JSON-LD logo URL in root layout for the local
  /assets/images/logo.png we already ship.
- Drop img.rocket.new from the Next.js image-hosts allowlist;
  add supabase.onsethost.com so storage proxy URLs can render.

The DB has had zero rocket.new image refs for weeks; this finishes
the job in the codebase so no page on broadcastbeat.com can render
a rocket-hosted asset anymore.
2026-05-20 06:14:02 +00:00
Ryan Salazar
9eadda4b9e authors: real article list from DB by author_name + version bump
Authors page was rendering a hardcoded mock article list per slug,
with Ryan Salazar's slug falling through to the staff-reporter mock
list (847 fake articles). New behaviour:

  - GET /api/author/[slug] returns ALL real articles where
    bb.wp_imported_posts.author_name matches the canonical name(s)
    for that slug. Slug → names map:
      ryan-salazar     → ["Ryan Salazar"]
      james-whitfield  → ["James Whitfield"]
      sarah-chen       → ["Sarah Chen"]
      … etc.
    Unknown slugs fall back to a Title-Cased guess.
    Response: { total, articles[] }, cached 5min / SWR 10min.

  - /authors/[slug] now fetches that endpoint on mount and replaces
    the hardcoded list with the live one. Article count shown on the
    profile header + sidebar is now the live total (not the
    hardcoded 1200/847/etc).

  - Version badge in the status bar bumped v4.2.1 → v2.0.0 to match
    the actual public release.

Ryan Salazar's profile now correctly shows his 135 real bylines from
WordPress instead of the staff-reporter mock 847.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 06:04:30 +00:00
Ryan Salazar
6e9c1b59bd authors: add ryan-salazar profile with real bio; fix staff-reporter joinedDate
/authors/ryan-salazar was falling through to the generic
  'staff-reporter' entry — which is why the page rendered as
  "Broadcast Beat / Broadcast Beat Editorial Team / Joined January
  2020". Add a proper ryan-salazar entry to the authors map:

    name        Ryan Salazar
    title       Founder of Broadcast Beat
    joinedDate  December 2013
    bio         (~150 words, sourced from public bios on theorg.com,
                 JVC casebook, AV Beat, Broadcast Beat Q&A — covers
                 ProComm 1997 → SunSpots CTO → AV Beat 2005 → SMPTE
                 LIVE 2014 → Broadcast Beat 2013 → Broadcast Beat
                 Studios 2020 → NAB Show LIVE executive producer.)

  Also corrected the staff-reporter joinedDate to "December 2013"
  (matches actual founding date of the publication).

  Separately: bb.user_profiles.role for Ryan was 'administrator' and
  has been updated to 'founder' so any role-based displays show the
  right title.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 06:01:55 +00:00
Ryan Salazar
cb00f7f343 search: return all matches (not just 100) + add typeahead autocomplete
- searchLegacyArticles() default limit: 50 → 5000. /search page
    explicitly passes 5000 too. ilike on title/excerpt/author is fast
    enough on the 29k-row archive that returning everything is fine.

  - New API route /api/search/suggest?q=...&limit=8 returns the top
    matching article titles + category + slug, with a 30-second CDN
    cache + 60-second stale-while-revalidate.

  - The search input in the Browse bar now debounces (150ms) and
    fires that API per keystroke once 2+ chars are typed. Matches
    appear in a styled dropdown below the input — click to navigate
    directly to /news/<slug>, or press Enter to fall through to the
    full /search page. "See all results for …" link in the dropdown
    footer.

  - Keyboard: Escape closes the dropdown; aria-autocomplete + role
    listbox/option for accessibility.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 06:00:10 +00:00
Ryan Salazar
e8552794fe categories: update SECTION_CATEGORIES map to match the AI-derived taxonomy
After the AI re-categorization, bb.wp_imported_posts.category holds the
new taxonomy: 'Industry News', 'Show Coverage', 'Cameras & Capture',
'Streaming & IP', 'Audio', 'Live Production', 'Cloud & Workflow',
'Post Production', 'Broadcast Production', 'AI & Automation'.

The map that drives /gear, /technology, /show-coverage, /news page
queries still referenced the OLD taxonomy ('Gear', 'Reviews',
'Technology', 'Streaming') — so /gear and /technology effectively
returned nothing-matched and fell through to the catch-all section,
causing the duplicate-posts-across-sections behaviour the user
reported.

  /gear         → Cameras & Capture, Audio
  /technology   → Streaming & IP, Cloud & Workflow, AI & Automation,
                  Live Production, Post Production, Broadcast Production
  /show-coverage→ Show Coverage
  /news         → Industry News

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 05:55:43 +00:00
Ryan Salazar
81913320cb align: unify container width to max-w-container across all pages; fix banner edge alignment
The Featured-story card's right edge wasn't lining up with any of the
ad banners below or beside it on the homepage. Two root causes:

  1. Pages I redesigned (/about, /about/contact, /about/team,
     /about/advertise, /show-coverage/[slug]) used `max-w-6xl px-6`
     (1152px / 24px-pad). Every other page uses `max-w-container
     px-4` (1200px / 16px-pad). Pages didn't line up with the
     header leaderboard or with each other.

  2. ArticleFeed used a flexible 12-col grid where lg:col-span-4
     ends up ~280px wide (depending on container) — so the 300x250
     image inside the sidebar didn't extend to the column's right
     edge, leaving a visual gap relative to the full-width Featured
     card above.

Fix:
  - All redesigned pages now use `max-w-container mx-auto px-4`.
  - ArticleFeed switches to `lg:grid-cols-[1fr,300px]` so the sidebar
    column is *exactly* 300px wide. The 300x250 ads fill that column
    flush, so their right edge meets the container's right padding
    edge — same x-position as the Featured card's right edge above.

Also linkified the "Relevant Media Properties" copyright string in
Footer.tsx → opens https://www.relevantmediaproperties.com/ in a new
tab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 05:55:10 +00:00
Ryan Salazar
a1829e7628 ads + i18n: forum/newsletter banners; smarter language autodetect
Forum:
  - /forum landing: switch to 2-col layout, add SidebarAdStack
    (300x600 + every 300x250) on the right.
  - /forum/thread/[id]: insert one 300x250 "Sponsored" tile after
    every 4 replies (idx 3, 7, 11 …). Trailing-ad guard so the last
    reply isn't followed by an ad with no replies after it. Cycles
    through the full ADS_300X250 pool by index so every banner gets
    impressions across long threads.

Newsletter:
  - /newsletter/archive: drop SidebarAdStack into the existing
    right rail above the signup CTA + topic browser.

LanguageSwitcher autodetect (US default for US users was failing):
  1. Timezone override: if the user is in America/*, Pacific/Honolulu,
     Pacific/Auckland|Wellington, Australia/*, Europe/London|Dublin,
     or Atlantic/Reykjavik — always return 'en'. Fixes US-based
     users whose OS locale happened to be set to a non-English value.
  2. Otherwise walk navigator.languages (the full preference list,
     not just navigator.language[0]) and pick the first base code we
     support (en/es/pt/fr/de/zh/ja/ar/hi).
  3. Fallback to 'en'.

  All non-en selections still route through Google Translate's hosted
  proxy on the .translate.goog subdomain — search engines and US
  visitors continue to only ever see English on broadcastbeat.com.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 05:52:00 +00:00
Ryan Salazar
b4eb56c81d news: SidebarAdStack in /news sidebar (300x600 + every 300x250); 'Industry News' title 2026-05-20 05:45:20 +00:00
Ryan Salazar
4e867e2819 branding: 'Latest Broadcast News' → 'Industry News'; 'Broadcast Technology' → 'Production Technology' (page + footer + metadata) 2026-05-20 05:43:58 +00:00
Ryan Salazar
d2d941c576 about/show-coverage: redesign with sidebar (300x600 + rotating 300x250s)
Switch the bare single-column layouts on these pages to a two-column
grid that pulls in <SidebarAdStack /> on the right (pinned 300x600 +
every 300x250 rotated). Header keeps the leaderboard above. The
in-content 728x90 banners added earlier are removed since the sidebar
now carries the ad load (avoids stacking two of the same unit on the
page).

Pages updated:
  - /show-coverage/[slug]   event detail
  - /about
  - /about/contact
  - /about/team
  - /about/advertise (also gets a real-banner grid in "Available units":
    the actual 728x90, 300x600, and 300x250 sample creatives are
    rendered next to each unit's description, so advertisers see what
    the inventory looks like in production rather than just a label).

The /about pages also got a small refresh: serif H1 + accent-blue
"Broadcast Beat" eyebrow line + bordered content blocks, bringing the
look closer to the cinematic homepage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 05:43:02 +00:00
Ryan Salazar
4e1e5c22e8 header: align EventsDropdown + AboutDropdown buttons with new Browse bar height (fix alignment) 2026-05-20 05:40:22 +00:00
Ryan Salazar
c5d4c1d075 header/footer chrome polish: logo in footer, search in NAV bar, NAV bar restyle
- Footer bottom: replace the plain "Broadcast Beat" text wordmark
    with the same logo image used in the top-left header (so footer
    matches header brand mark).

  - Move the site-wide search input out of the top sticky row and
    into the Browse bar that sits below the 728x90 leaderboard. Top
    row is now just logo / reading-list / notification bell.

  - Browse bar redesign for personality + alignment:
      * 48px tall, dark navy gradient with a 2px accent-blue bottom
        border and a horizontal sheen across the top edge
      * Search input (pill shape, focus-glow ring) sits flush left
      * Menu items (Events ▾, Newsletter, Forum, About ▾) are now
        right-aligned via flex justify-between
      * Each menu item is uppercase + tracked, hover lights an
        accent-blue underline that scales in from the left
      * EventsDropdown and AboutDropdown buttons now use the same
        .bb-browse-item class — fixes the vertical alignment issue
        where they were rendering at the old 60px nav-link-bb height
        while the new bar is 48px.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 05:40:04 +00:00
Ryan Salazar
3bb27e2e0a ads/chrome: banners on /search and /reading-list; wrap /contributor with site chrome
- /search and /reading-list already had Header + Footer; add a
    single in-content 728x90 banner at the bottom of each main column
    (gated on ADS_728X90.length > 0, hidden on mobile).

  - /contributor was a bare dashboard with sidebar + main, no site
    chrome at all. Wrap it in the standard Header + Footer; drop the
    inner sidebar/main wrapper from min-h-screen to a natural-height
    flex so the dashboard sits below the header instead of competing
    with it for full viewport. Header brings the site-wide 728x90
    leaderboard with it, so contributors see the same ad inventory
    as the public site without a second in-content banner cluttering
    the dashboard panes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 05:07:49 +00:00
Ryan Salazar
a1d5ecf51d ads: add in-content 728x90 banner to gear, technology, forum, authors
These four landing pages already had the site-wide header leaderboard
via <Header />, but no additional in-content banner. Add one 728x90
slot at the bottom of the main column on each, gated on
ADS_728X90.length > 0 and hidden on mobile to match the header
banner's display rules.

  - /gear            — category landing
  - /technology      — category landing
  - /forum           — community landing
  - /authors/[slug]  — author profile

Brings these pages in line with /show-coverage and /about/* which got
the same treatment earlier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 04:15:45 +00:00
Ryan Salazar
1b0b541c40 about: add Header/Footer to /about/contact + /about/advertise; banners on all three
/about/contact and /about/advertise were rendering bare <main> tags
with no site chrome at all — no header, no footer, no banners. Both
are now wrapped with the standard
  <div className="min-h-screen bg-background"><Header />…<Footer /></div>
shell that the rest of the site uses, which auto-brings the site-wide
728x90 leaderboard with it.

/about/team already had Header+Footer but no in-content banner.

In addition to the header leaderboard, each of the three pages now
also renders one in-content 728x90 banner (gated on
ADS_728X90.length > 0, hidden on mobile to match the header banner's
display rules). This brings the about/* pages in line with the ad
inventory of /show-coverage and the other category landing pages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 03:59:37 +00:00
Ryan Salazar
751eca2f16 show-coverage: add site Header + Footer + in-content 728x90 banner
Both /show-coverage and /show-coverage/[slug] were rendering bare
<main> elements with no site chrome — no top nav, no leaderboard ad
above the page, no footer, no in-content banners.

- Wrap each page in <div className="min-h-screen bg-background">
  <Header />…<Footer /></div>. <Header /> already brings the site-wide
  728x90 leaderboard with it (see Header.tsx footer of the component)
  so that banner appears automatically above the page content.
- Add one in-content 728x90 banner per page (between Upcoming/Past on
  the index, and between event header / Coverage list on the detail
  page) so the show-coverage pages carry the same ad inventory as
  other sections.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 03:56:23 +00:00
Ryan Salazar
7f0675a0e3 header: restore bb-browse-bar styles after DoubleTicker revert
The DoubleTicker commit (a52f8ad, since reverted) had bundled the
.bb-browse-bar / .bb-browse-row / .bb-browse-item CSS rules together
with the ticker CSS. Reverting the ticker removed the Browse bar
styles too, but Header.tsx still uses those classes for the
below-the-leaderboard nav. Re-add just the Browse bar selectors so
that nav renders correctly post-revert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 03:54:12 +00:00
Ryan Salazar
b1378de6d6 Revert "home: replace single LiveWire ticker with stacked dual ticker"
This reverts commit a52f8adb21.
2026-05-20 03:53:31 +00:00
Ryan Salazar
8890e61c2e i18n: kill spurious translate popup; make language pulldown actually translate
Three connected fixes:

1) Stop the unwanted "translate this page to Portuguese?" popup that
   some US users were seeing. Chrome's content-language detector was
   mis-firing on imported BB articles that mention Portuguese/Spanish
   company/person names. Add to layout <head>:
     <meta name="google" content="notranslate" />
     <meta http-equiv="Content-Language" content="en" />
     <meta name="robots" content="index, follow" />
   Together with the existing <html lang="en"> this is the standard
   "this site is English, don't offer to translate" signal.

2) Wire the LanguageSwitcher pulldown to actually do something. It used
   to set a cookie + flip <html lang> with no effect on visible
   content. Now picking a non-English language redirects the current
   URL to Google Translate's hosted proxy (e.g.
     https://broadcastbeat-com.translate.goog/<path>?_x_tr_sl=en
       &_x_tr_tl=es&_x_tr_hl=en
   ), which renders the entire page translated server-side by Google
   and preserves the user's session as they navigate. English stays
   on the canonical broadcastbeat.com host.

3) SEO posture: English is the only indexable version. The Google
   Translate proxy lives on a different (Google-owned) domain so US
   browsers and Googlebot only ever see the English content. The
   alternates.languages hreflang block stays absent — re-add only once
   real translated routes exist (Phase D).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 23:10:05 +00:00
Ryan Salazar
8e7e152422 header: move primary nav to a Browse bar below the 728x90 leaderboard
The main sticky top row now holds only the logo, Reading List icon,
notification bell, and search box. The Events dropdown, navLinks
(Newsletter, Forum), and About dropdown were lifted out of the top
row and re-rendered in a new "Browse bar" that sits directly below
the header leaderboard banner.

Visual result:
  [ top utility bar (account / admin) ]
  [ logo                          📑 🔔 🔍 ]
  [ 728x90 leaderboard ]
  [ Events ▾  Newsletter  Forum  About ▾ ]   ← new Browse bar
  [ LIVE WIRE ····                       ]
  [ RECENT FORUM POSTS ····              ]

The mobile hamburger drawer is unchanged — it already keeps all the
nav items consolidated for small screens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 23:08:05 +00:00
Ryan Salazar
a52f8adb21 home: replace single LiveWire ticker with stacked dual ticker
Two horizontal lanes stacked on top of each other:

  Lane 1  LIVE WIRE           red gradient label, pulsing white dot,
                              latest 14 published articles, 65s loop
  Lane 2  RECENT FORUM POSTS  green gradient label, pulsing white dot,
                              latest 14 forum threads, 82s loop

The two lanes use intentionally different animation durations (65s vs
82s) so they never sync up — feels alive like a real wire room. Both
loops use CSS translateX with a duplicated content track for seamless
infinite scroll. Edges fade via mask-image so items dissolve in/out
rather than appearing on a hard line.

Both lanes pause on hover (the whole row) so users can stop motion
to read or click. Each item is a real <Link>; clicking opens the
article or thread page.

Server component — data is fetched once at request time from
bb.wp_imported_posts and bb.forum_threads, cached at the page's
revalidate boundary. No client JS needed.

Old LiveWireTicker.tsx is removed (orphaned, not referenced elsewhere).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 23:06:15 +00:00
Ryan Salazar
b21ac70e0c ui(consent): persist cookie-banner choice in a cookie (365-day) + localStorage
Previously the banner only wrote bb_cookie_consent to localStorage. Many
browsers wipe localStorage in strict-tracking-prevention modes, on
"clear site data on close", and in some private-browsing-adjacent
profiles — so the banner re-appeared on every visit even after the
user clicked Accept or Decline.

Now writes to BOTH a `bb_cookie_consent` cookie (path=/, max-age=365d,
SameSite=Lax) and localStorage. Reads from cookie first, falls back to
localStorage for backward compatibility with prior visitors. Banner
stays hidden for 365 days after a decision is made.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 22:53:19 +00:00
Ryan Salazar
28a910a7f8 ui: drop '12,847 indexed' badge in Ask BB AI; remove rocket.new logo from footer
Two unrelated tidy-ups, bundled:

- src/components/AskBBAI.tsx: remove the "12,847 indexed" subtitle next
  to the Ask BB AI floating-action button (placeholder/mock text from
  the original mockup; the real index count isn't wired in).

- src/components/Footer.tsx: the footer-bottom logo was sourcing an
  image from img.rocket.new — a third-party CDN unrelated to BB
  (artifact from the initial rocket.new export). Replace with a plain
  text wordmark Link to /home-page. No third-party image, same UX.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 20:02:47 +00:00
Ryan Salazar
a6ab915f2d chore: empty commit to retrigger Coolify webhook for ab47136 2026-05-19 19:51:39 +00:00
Ryan Salazar
ab47136965 home(feed): rename "View Original »" link to "Read More..."
The Latest section was showing "View Original »" for imported posts and
opening every click in a new tab, even though the /api/public/posts
endpoint doesn't expose an externalUrl — every link actually went to
the internal /articles/<slug> page. Two changes:

- Link text is now "Read More..." for every article in the feed.
- target="_blank" only applies when article.externalUrl is actually
  populated (currently never, but keeps the door open for cross-site
  syndication later). Internal /articles/<slug> links now open in the
  same tab as the user expects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:17:34 +00:00
Ryan Salazar
488a381aed home: hide dates on Featured posts site-wide; remove BB-AI trends sidebar
Dates
- FeaturedBentoFromDb hero + 4-up rail no longer render `ago(wp_published_at)`.
  The whole component renders only Featured-category posts (filter is
  category ILIKE 'featured'), so dropping the date display globally inside
  it satisfies the "Featured posts have no date" requirement.
- /gear and /technology category cards: wrap the existing `{article.date}`
  output in `{article.category !== "Featured" && ...}` so only non-Featured
  posts continue to show their publish date.
- All other category landing pages, /news/<slug> detail, ArticleFeed, and
  LiveWireTicker either don't render a visible article date or only use it
  for SEO/structured-data fields (kept).

Trends sidebar
- Remove `BB AI · detected trends` block from the homepage (between
  ArticleFeed and NewsletterSignup). Drop the unused import and delete
  the orphaned TrendsSidebar.tsx component file. The detect-trends data
  fetch and `bb_pulse_dot` mini-widget are gone from the page; nothing
  else referenced them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:47:00 +00:00
Ryan Salazar
308b27ec3e fix: use correct fallback image path for legacy articles 2026-05-18 22:07:30 +00:00
Ryan Salazar
fe0edf5c02 home(feed): drop LEGACY category label, vertical separator, and Imported source badge
The article-row metadata strip rendered three things side-by-side:
  {category} | <SourceBadge source>

For imported WP posts whose `category` column was the placeholder string
"legacy", this produced a meaningless "LEGACY | IMPORTED" row on the
homepage. Now:

- Hide the category text entirely when value is "legacy" (any case);
  it falls back to nothing while the AI-derived category propagates
  in a separate data pass.
- Drop the vertical pipe separator.
- Remove the yellow "IMPORTED" SourceBadge — the article's category
  is the user-relevant signal, not its data-source provenance.

The SourceBadge function is now unused and removed. The source-filter
control in the advanced filters panel is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:01:46 +00:00
83579523d1 forum: rebrand to The Crew Lounge + scaffold Credits system
UI (targeted edits, no identifier renames):
- forum/layout.tsx: metadata title/OG/twitter/schema.org + breadcrumb name → The Crew Lounge
- forum/page.tsx: H1 + intro copy → The Crew Lounge (swapped 💬 for /logos/crew-lounge-logo.svg)

New assets:
- public/logos/crew-lounge-logo.svg — film-reel + CREW logo, #5B7C8D → #8FB0C3 gradient
- src/components/crew-lounge/CreditsDisplay.tsx — per-category totals + Crew Leader badge
- src/components/crew-lounge/CrewLoungeLogo.tsx — wrapper component

Database (4 new tables on supabase01, all TIMESTAMPTZ, IF NOT EXISTS guards):
- bb.user_credits — per-user/per-category daily credit ledger
- bb.user_credits_totals — cached leaderboard totals
- bb.crew_leader_badges — awarded at 10+ credits/category
- bb.credits_monthly_snapshot — monthly leaderboard captures

Docs:
- docs/guides/FORUM_SETUP_COMPLETE.md — replication guide
- docs/guides/FORUM_SETUP_CHECKLIST.md — per-property checklist

Forum tables (bb.forum_*) and TypeScript identifiers untouched.
Routes (/forum) unchanged — display-name rebrand only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 16:08:21 +00:00
claude-code
82ae43ccf5 feat: allow cron-secret to invoke company-coverage scan with service role 2026-05-16 15:01:30 +00:00
claude-code
39311a4401 feat: wire up The Latest feed — fetch imported WP posts from new /api/public/posts 2026-05-16 12:56:16 +00:00
e4e74a9059 home: add horizontal divider line in Featured section header (matches The Latest)
Switches the Featured header row from justify-between to gap-3 + flex-wrap
so a 1px <div className="flex-1 h-px bg-[#2a2a2a]" /> can fill between the
label and the "All featured →" link. Same rhythm as The Latest header in
ArticleFeed.tsx:276-277.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 09:36:07 +00:00
24a32b9757 forum: sync metadata + schema.org with the new Industry Forum branding
Updates the forum's <head> SEO/social metadata to match the visible header.
Old "Community Forum" / "broadcast engineering professionals" copy was
leaking into Google + LinkedIn / Twitter Card previews after yesterday's
visible-text rebrand.

- metadata.title, openGraph.title, twitter.title: Community → Industry Forum
- All four description strings broadened to "broadcast, motion picture and
  post production professionals worldwide"
- Schema.org WebPage name + description aligned

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 09:31:17 +00:00
e88f7ec25e forum: rebrand header — Community → Industry, broaden audience copy
- "Community Forum" → "Industry Forum"
- "Connect with broadcast engineering professionals" → "Connect with broadcast, motion picture and post production professionals worldwide"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 09:23:53 +00:00
1c5ae77ca4 home: remove "AI Picks For You" section from homepage
Drops <AISuggestedArticles variant="compact"> + its ScrollRevealSection
wrapper from /home-page. Component file stays — still rendered on /news
(NewsPageClient uses variant="full").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 09:21:32 +00:00