Mobile readers previously saw zero ads above the bottom of the feed
(header leaderboard is hidden md:block, sidebar stack pushes below the
feed at <lg breakpoints). This adds a 300x250 ad block after every 4th
article in <ArticleFeed>, wrapped in md:hidden so tablet/desktop are
untouched (they already get SidebarAdStack to the right of the feed).
- Ad pool: rotateAll('300x250') from src/lib/ads.ts (DB-backed, falls
back to the 7-ad hardcoded pool)
- Cycles through the pool if the feed is longer than the pool
- Skips the insert after the very last article
- Labelled "Sponsored", divide-y separator inherited from parent
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the bento's old hero+side-rail layout with a full-width cinematic
21:9 hero on top + 4-up vertical-card rail below. Single DB query, single
component. Standalone FeatureStoryHero component removed (was rendered
above the bento; design now lives inside the bento itself).
- FeaturedBentoFromDb.tsx: new hero (gradient overlay, Feature Story badge,
large overlaid title, "Read full story" cue) + 4-card grid below
- home-page/page.tsx: drop <FeatureStoryHero /> import + JSX block
- src/components/FeatureStoryHero.tsx: deleted (composition replaced by
inline JSX in the bento for single-query efficiency)
Accent uses var(--color-text-info, #60a5fa) — the existing blue from the
codebase, matching the rest of the post-Slice-4-revert site.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the four accent-family hex codes site-wide with the teal palette
(keeps the dark theme; only swaps the accent family, not the dark base):
#3b82f6 (accent primary CTA) → #5B7C8D (teal) [839×]
#2563eb (accent-dark / hover) → #4A6473 (darker teal) [44×]
#60a5fa (info link, lighter) → #8FB0C3 (mid teal) [68×]
#1e3a5f (accent-muted bg) → #2F4F5F (navy) [44×]
tailwind.config.js tokens updated to match (accent/accent-dark/accent-muted).
Sweep also covers tailwind.css. 108 files touched. The dark base (#0d0d0d,
#111, #161616, #1a1a1a etc.) is intentionally untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New component src/components/FeatureStoryHero.tsx renders a wide 21:9 hero
of the top-pinned featured article (same query shape as FeaturedBentoFromDb).
Wired into home-page/page.tsx above the bento. Teal accent (#5B7C8D) used
intentionally — Slice 4 will sweep the rest of the site to match.
Bento remains intact and may share its top article with the hero; that's
the magazine "lead story emphasized" treatment, not a bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Header.tsx: bare linkedin/instagram/facebook URLs → /broadcastbeat handles; remove TwitterXIcon from social bar + import (Footer retains TwitterXIcon per "DO NOT OVERRIDE" marker)
- AboutDropdown.tsx: drop "Press kit" item (the page itself stays at /about/press-kit, just delisted from menu)
- SystemStatusBar.tsx: remove "Index online" pulse-dot and "N wire sources" span (keep articles count + events count + build version)
- /about, /about/team, /about/press-kit: wrap in Header+Footer (previously rendered bare <main>)
- /newsletter/page.tsx (new): redirect to /newsletter/archive — Header was 404'ing on /newsletter
- /rss/route.ts (new): dynamic RSS feed reading from legacy-source news section; replaces 404 at /rss
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per Ryan 2026-05-15:
- Removed Vector pass footer (no audience value)
- Removed PHASE 6b comment
- Removed corner brackets + monitoring subtitle + rank prefix
- Top 5 entities now show as Sony +12 / Riedel +8 — entity name
serif + count mono, right-aligned
- Background #F5EFEB (beige; #F5EFFEB had 7 hex chars, interpreted as
#F5EFEB) via new --color-trends-bg token
- Text #2F4F5B (navy) via new --color-trends-text token
- Tokens added to redesign-tokens.css palette section
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the 5-slide FeaturedCarouselServer that lost the Sony hero +
side-rail layout. New FeaturedBentoFromDb pulls every category=Featured
row, sorts featured=true (pinned hero) first then by published date.
- Hero (left, 2 columns): Sony BRC-AM7 + HXC-FZ90 — pinned via featured=true
- Side rail (right): next 4 Featured-category posts (BVM-HX1710, MCRs/NOCs,
Tessera SQ200, Backlight Iconik)
Old carousel kept on disk for future use. Layout matches the prior
FeaturedBento.tsx visual structure (hero + 4 small cards right).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tables (Phase B+):
bb.ad_impressions — every render = a row. is_bot + viewport flags
for filtering. No unique constraints — gross
counts per Ry an spec.
bb.ad_clicks — every /r/[slug] hit = a row.
bb.ad_campaign_clients — schema only; future client reporting.
FK target is bb.banner_creatives (the renderer table from 5/15 banner
refresh). bb.ad_campaigns is the AdOps billing schema and unrelated.
Routes:
GET /r/[slug] — log click, 302 to bb.banner_creatives.click_url
POST /api/track/impression — record impression (slug | campaign_id),
used by client beacon
AdImage rewrite:
- link href is /r/{slug} target=_blank rel=sponsored noopener noreferrer
- sendBeacon on mount fires impression (viewport=false)
- additional sendBeacon on IntersectionObserver ≥0.5 (viewport=true)
- removed direct supabase.from('banner_analytics').insert path
Ad type carries slug now; FALLBACK list + DB mapper both populate it.
/admin/banners:
per-row stats — 24h, 7d, lifetime impressions/clicks/CTR
link to /admin/banners/[id]/analytics
/admin/banners/[id]/analytics (new):
recharts line charts for impressions + clicks (30d), top page paths,
top referrer domains, bot/human toggle.
Tower Products orphan image file removed; banner was not in any active
table or in code (already off rotation).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bb.banner_creatives row also inserted (AdSanity 191740, click_url
https://telycam.com/, dates 2024-05-07 → 2026-12-31, status active). The
creative was already in public/legacy/ads/ from the pre-Phase-B banner
pool — just needed the DB row + FALLBACK entry so SidebarAdStack picks
it up before the DB cache fills.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend:
bb.banner_creatives (new) — image_path, click_url, size, start/end_date,
status, adsanity_source_id. Seeded with 8 banners from broadcas_temp
WP-export DB (post_type=ads): LiveU 728x90, Blackmagic DaVinci Resolve
20 (300x600), Studio Hero, Magewell Pro-Convert, LiveU PAYG, AJA
ColorBox, Zixi, Lectrosonics (all 300x250). Dates and click URLs come
from the AdSanity rows; current creatives downloaded from Wayback
snapshot 20260316123737.
src/lib/ads.ts:
- Reads bb.banner_creatives via anon supabase-js client, gated by
status='active' AND start<=NOW()<=end.
- In-process 5-min TTL cache with stale-while-revalidate. Module load
primes the cache; clients see fallback on first hit, live DB on
subsequent.
- Hardcoded FALLBACK list mirrors the seeded rows so the site keeps
rendering banners if Supabase is unreachable during deploy.
- Public helpers (rotateAll, pickAds, ADS_728X90, ADS_300X250,
FIXED_300X600) remain synchronous so existing client component
callsites work unchanged.
/admin/banners:
Admin-only list + inline editor for every banner_creatives row:
name, click URL, start/end date, status (active|scheduled|paused|
expired). PATCH via /api/admin/banners/[id] — service_role bypass for
the write, user_profiles.role admin/administrator for the gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Magewell was removed when Adsanity went away; PENDING_ads.md flagged it
along with AJA/Zixi/Lectrosonics/Opengear/Studio Hero for real creatives.
This restores Magewell with an in-house SVG placeholder (red/dark Magewell
brand color, wordmark + tagline + CTA chip) so the sidebar 300x250 stack
gets three rotating entries again instead of two. Real Magewell creative
to land when sourced from the vendor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
src/lib/ai/quality-gates.ts:
- Read bb.banned_terms with the ban_level column so admins can mark
a term as hard (instant fail) or soft (score nudge).
- Hard hits join the hard-coded AI_TELLS list to instant-fail the
rewrite (status=failed_quality, regenerate).
- Soft hits each contribute +0.05 to the heuristic AI-detector score,
capped at +0.30, so a borderline-slop article can be tipped over
the 0.65 threshold without single-handedly being rejected.
- Multi-word phrases are matched as substrings; single words use a
boundary regex that handles hyphenated terms cleanly.
- QualityGateResult now exposes softWordsFound alongside the existing
bannedWordsFound; failure_reason text includes the soft list when
the score trip is what failed.
44 terms seeded in bb.banned_terms (23 hard hype words like
"revolutionary", "industry-leading", "world-class", plus 7 PR-speak
openers like "pleased to announce"; 21 soft like "innovative",
"leverages", "robust", "transform").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
src/app/api/admin/review-queue/[id]/route.ts
- Call revalidatePath on /news, /news/<slug>, /articles/<slug>, /home-page,
section pages, and /sitemap.xml on approve/reject. The /news listing is
ISR-cached (revalidate=1800) so approval would otherwise be invisible
for up to 30 minutes; this flushes the cache immediately.
src/app/api/ai/submit/route.ts
- Reject submissions where rawTitle/title is whitespace or rawContent/body
is empty after stripping HTML. Previously empty bodies fell through into
the LLM and Claude dutifully wrote a 400-word piece about the empty
submission. Hard-cap at the gate to avoid the spend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
src/lib/articles/legacy-source.ts
- getLegacyArticleBySlug, getLegacyArticlesBySection, searchLegacyArticles,
getLegacyRecentSlugs, getLegacyRecentSitemapEntries: each now UNIONs
bb.ai_rewritten_articles (status=published) with bb.wp_imported_posts.
Rewrites carry persona attribution (author/avatar/title) via a
persona:ai_personas(slug,name,beat,avatar_url) embed.
- Section routing for rewrites is by category, not tags (rewrites do not
use the WP tag taxonomy — they have category strings written by Claude).
src/app/api/ai/submit/route.ts
- Bearer auth via BB_INGEST_BEARER (preferred) or x-internal-key (legacy)
- Polymorphic payload: accepts native {rawTitle, rawContent, ...} OR the
distribute-rmp shape {title, body, contributor:{...}, attachments,
submission_id, ...}. Distribute attachments + featured image survive in
bb.ai_original_submissions.metadata.
src/app/api/ai/rewrite/route.ts
- Same bearer auth pattern as submit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pluggable rewrite client (Anthropic default, ollama/openrouter stubs)
using claude-opus-4-7 with prompt caching on system prompt + style guide.
Routes the raw submission through:
1. bb.ai_original_submissions — raw never edited
2. bb.ai_rewrite_jobs — every attempt logged with token counts
3. quality gates — AI-tell banned-word check + heuristic
AI-detector score (threshold 0.65)
4. regenerate up to 3x — on banned words or detector trip
5. bb.ai_rewritten_articles — status ai_rewritten_pending_review
6. /admin/review-queue — list + detail page with approve / reject /
needs-human actions; on approve, the
original submission is FK-linked to the
published rewrite
New files only — no existing routes or contributor flow modified.
Persona auto-selection routes by beat keywords; 6 personas seeded
(marcus-halverson, elena-vasquez, darren-okafor, sarah-quinn, mike-trayton,
priya-rao) with SVG monogram avatars in public/assets/images/personas/.
Smoke test (Matrox / ST 2110 sample press release):
- persona auto-routed to darren-okafor (protocols-specs)
- anthropic 22.3s, in=2616 / out=1372 tokens
- gates passed first try, score=0.000, no banned words
- review_queue row created
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Header.tsx: remove "SIGN IN" and "REGISTER" buttons from the top
utility bar and the mobile drawer. Auth for PR firms / writers lives
at distribution.relevantmediaproperties.com now; the local /client-login
and /register routes are no longer the right entry point.
- src/middleware.ts (new): 301-redirect /wp-login, /wp-login.php,
/wp-admin, /wp-admin/*, /login, /client-login, and /register to
https://distribution.relevantmediaproperties.com/login so existing
bookmarks and old WP-era links land somewhere useful instead of 404.
- Newsletter signup form, social icons, country selector all retained.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
P0-1 (featured-image fallback path):
- AppImage.tsx: default fallbackSrc changed from /assets/images/no_image.png
to /assets/images/article-placeholder.svg, so any article image that
loads but 404s gets the new clean dark gradient placeholder instead of
the old grey BB_Gray-style box. The smoke-test article
(matrox-video-and-amagi-collaborate-...) had a featured_image URL that
404s on broadcastbeat.com; it now renders the new placeholder.
P0-1 batch (Wayback recovery) is NOT done — see PENDING_ads.md. The
attempted backfill via Wayback found no snapshots for the Matrox smoke
article (it is a 2026 NAB story Wayback never crawled), and the batch
loop tripped a 403 from one of its outbound calls. Proper backfill
needs per-vendor press-kit scraping and a new bb-media storage bucket.
PENDING_ads.md updated with: featured-image backfill TODOs, the banners
that still need local creatives, the hardcoded slug refactor for
NewsTicker / FeaturedBento / ArticleFeed, and the SEO re-enable gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
P0-2 (Adsanity iframe path removed):
- AdImage.tsx: deleted the iframe branch and the ADSANITY_CAMPAIGN_MAP
fallback. AdImage now renders local images only; banners without a
src render nothing.
- ads.ts: removed Ad.adsanity_id from the interface. Pruned AJA, Zixi,
Lectrosonics, Opengear, Studio Hero, Magewell 300x250 and Blackmagic
300x600 — they relied on ads.broadcastbeat.com which does not resolve.
Surviving live banners: LiveU 728x90, LiveU PAYG 300x250, Telycam 300x250.
- SidebarAdStack: key no longer references the removed field.
- No banner_analytics rows reference ads.broadcastbeat.com.
Phase A (SEO baseline):
- robots.ts: fallback base URL now broadcastbeat.com (was rocket staging URL).
- sitemap.ts: fallback base URL now broadcastbeat.com (was bb-staging).
- news/[slug]/page.tsx and articles/[slug]/page.tsx: same fallback fix.
JSON-LD now uses ISO 8601 for datePublished, absolute image URL via
SITE_URL prefix, and publisher.logo points at /assets/images/logo.png
instead of the 404-ing /legacy/site/broadcastbeat-logo.png.
- layout.tsx: removed the /en /es /pt /fr /de /zh /ja /ar /hi hreflang
alternates - those routes 404 today; Phase D will reinstate. Replaced
rocket.new Organization schema URL with broadcastbeat.com. Set
robots index/follow=false until Phase B-D land and Rich Results passes.
Featured-image fallback hardening:
- legacy-source: rowToArticle now treats BB_Gray, no_image and placeholder
URLs as null and falls back to the new placeholder so dead images do not
leak through to the page.
- public/assets/images/article-placeholder.svg: new clean dark gradient
with BroadcastBeat wordmark, NOT no_image.png style.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Deploy-1 build failed because NewsPageClient now calls useSearchParams.
- src/app/news/page.tsx: wrap NewsPageClient in <Suspense> so static
generation can bail out to client rendering cleanly.
P0-6 (Featured carousel arrows):
- FeaturedBento: brighten arrow buttons (text-[#ccc] over bg-[#1a1a1a]
with #3a3a3a border; previously near-invisible #666 over the same
background), enlarge to w-8 h-8, hover fills to blue. Wrap the
arrow group with role/aria + Left/Right keyboard nav.
P0-7 (728x90 reposition):
- Header: move the leaderboard block from above the main nav to AFTER
the sticky nav, so it sits between the nav and The Beat ticker.
P0-8 (remove Latest search box):
- ArticleFeed: drop the in-section search input + clear button. The
top-right header search is now the sole search UI. searchQuery state
remains since the active-filter chip code still references it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
P0-3 (category page crash):
- Add src/app/error.tsx and src/app/global-error.tsx so any future render
error shows a real ref id instead of the bare client-exception toast.
- NewsPageClient: null-guard every field accessed during search/category/
sort filtering and article render (title, excerpt, author, tags,
category, date, image, alt). The user-visible link
/news?category=live-production now also hydrates filter state from
?category= and ?search= search-params, so the dropdown actually filters.
P0-4 (/articles/[slug] 404s):
- /articles/[slug] and /news/[slug]: on slug miss, redirect("/news")
instead of notFound(). Dead links from the hardcoded NewsTicker /
FeaturedBento / ArticleFeed arrays now land on the news index instead
of dead-ending on 404.
- legacy-source: add searchLegacyArticles() (title/excerpt/author ilike).
P0-5 (header search):
- Header: wire Enter on the desktop and mobile search inputs and make
both search icons clickable buttons. Submits to /search?q=<query>.
- New /search route: server component that runs searchLegacyArticles
and renders results in the same card style as /news.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Banner spec from Ryan:
- 728x90 header (every page): pool is now LiveU only; rotates when more
728x90 entries are added to ads.ts.
- 728x90 below main menu on article pages: removed (was the article-top
AdSlot stub).
- 728x90 footer (every page): replaces the old 300x250 rocket.new
placeholder in Footer.tsx. Rotates from the same ADS_728X90 pool.
- 300x250 sidebar: now renders EVERY entry in ADS_300X250 stacked
vertically, shuffled per page-view. Pool: AJA, Zixi, Telycam, LiveU,
Lectrosonics, Opengear, Studio Hero, Magewell. Adding more entries to
ads.ts auto-grows the stack.
- 300x600 fixed slot: Blackmagic (Adsanity ad-210571), top of sidebar,
never rotated.
Implementation:
- ads.ts: Ad interface now allows either src (local image) or adsanity_id
(iframe). New rotateAll() returns ALL ads of a size shuffled; pickAds
kept for ArticleBody bounded slots.
- AdImage.tsx: dropped the legacy ADSANITY_CAMPAIGN_MAP label fallback —
ads.ts is now the single source of truth, so Telycam (no adsanity_id)
renders its local GIF instead of the iframe.
- SidebarAdStack.tsx: rotateAll(300x250) instead of pickAds with a
count cap. count prop removed; callers updated.
- NewsArticleDetailClient.tsx: removed article-top 728x90 stub.
- Footer.tsx: 728x90 AdImage in place of the 300x250 placeholder.
- LeaderboardAd.tsx: deleted (was unused after the earlier home-page
cleanup).
- PENDING_ads.md: rewritten — Adsanity IDs to verify (Studio Hero,
Blackmagic) and optional local-file upgrades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove duplicate 728x90 LeaderboardAd below news ticker (per Ryan)
- Footer: remove NAB Official Media Partner badge
- Header: replace AdSlot stub with real AdImage rotation (3 verified 728x90s)
- Header: fix logo path from dead WP URL to local /assets/images/logo.png
- ads.ts: only include ads with verified real images (3x728x90, 2x300x250)
- ads.ts: remove 22 unrecovered ads — see PENDING_ads.md for upload list
- SidebarAdStack: handle null FIXED_300X600 gracefully (Blackmagic not recovered)
- public/legacy/ads/: 5 real ad images committed (Tower, Sony, LiveU 728x90; LiveU PAYG, Telycam 300x250)
- public/assets/images/logo.png: recovered from Wayback Machine
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migration creates bb.banner_analytics (mirrors article_analytics: public
insert, admin read), applied separately to Supabase.
AdImage becomes a client component that:
- Fires one 'impression' event per render via IntersectionObserver at 50%
visibility (avoids counting ads scrolled past at the fold).
- Fires a 'click' event on anchor click before the new tab opens.
Renders the same <a><AppImage/></a> as before — visually unchanged. Session
ID stored in sessionStorage as bb_session_id (random uuid, lazily created).
Both parents (SidebarAdStack, ArticleBody) were already 'use client', so no
boundary shifts. Tracking is best-effort and never blocks the user.
Top-bar nav changes:
- Authenticated user previously saw two separate links 'My Account' (-> /account)
and 'My Profile' (-> /profile/{userId}). Replaced with a single 'My Profile'
link -> /account.
- Admin block previously rendered 14 inline links (WP Import, Articles, Editorial,
Users, Analytics, Audit Log, Notifications, Newsletter, Forum Seed, Company
Tracker, Show Calendar, AI Console, Banned Terms, Dashboard) which sprawled
horizontally past the available width. 9 of those are now grouped under a
single TOOLS hover dropdown matching the existing NEWS/GEAR/SHOW
COVERAGE/TECHNOLOGY pattern (Tailwind group-hover + group-focus-within for
keyboard accessibility, role=menu / menuitem, aria-haspopup=true).
- Inline admin links retained per spec: WP Import, Articles, Editorial,
Newsletter.
- TOOLS submenu: Users, Analytics, Audit Log, Notifications, Forum Seed,
Company Tracker, Show Calendar, AI Console, Banned Terms.
The whole admin tier (inline + dropdown) remains gated by the existing isAdmin
check, so anonymous and non-admin users see nothing.
Logs success payload (sans secrets — just success/migrated/legacy/uid/
email/hasToken booleans) and the post-login document.cookie state so
DevTools can show whether the auth cookie actually landed in the
browser before the /contributor redirect runs.
The browser-side createBrowserClient was writing cookies with
'Secure; SameSite=None' while the server-side createServerClient
writes the same cookie names with 'SameSite=Lax' (no Secure flag —
Next.js cookies().set() default).
When the wp-login API set the auth cookie via NextResponse, the browser
stored it with SameSite=Lax. Then the browser supabase client (via
AuthContext or any subsequent auth call) tried to re-write the cookie
via document.cookie with Secure;SameSite=None. Browsers can't reconcile
this — depending on Chrome/Safari/Firefox version, you get either:
- duplicate cookies that confuse session detection
- the JS write silently failing because Secure;SameSite=None has
additional constraints
- the existing Lax cookie getting clobbered with attributes that
conflict with same-site navigation
Net effect: AuthContext on /contributor doesn't reliably see the
session even though the cookie exists, so /contributor's
'!loading && !user → router.push(/login)' check bounces the user
back to /client-login. Looks like login failed.
Fix: align the JS-side cookie attributes with the server-side defaults
(SameSite=Lax, no Secure). Both writes now produce the same cookie
shape, no conflict.
The previous attempt at hydrating the Supabase browser client via
supabase.auth.setSession() was racing against the server-set cookie.
The server cookie was written with Path=/; SameSite=Lax (no Secure
flag — Next.js cookies().set defaults), but the browser supabase
client's setAll handler writes cookies with Secure; SameSite=None.
The two writes for the same cookie name produced inconsistent state.
Drop setSession + router.push entirely. Instead, do a hard
window.location.href = '/contributor' redirect after a successful
POST. The server renders /contributor with the freshly-set auth
cookie; the browser supabase client boots up reading it from
document.cookie on page load. No client-side hydration race.
The wp-login API sets the auth cookie via NextResponse, but a
client-side router.push() doesn't always pick up the new cookie before
the destination page's AuthContext snapshots auth state — leading to a
brief 'logged out' flash on /contributor right after a successful
login.
Fix: after a successful POST to /api/auth/wp-login, call
supabase.auth.setSession({ access_token, refresh_token }) using the
session payload returned by the API. That hydrates the Supabase
browser client (and therefore AuthContext) immediately, so the next
render is authenticated. router.refresh() is also called to invalidate
the Next.js Router Cache for any stale RSC payloads.
Also adds console.error on both the !res.ok branch and the catch block
so login failures are inspectable in DevTools.
Previously the route ran phpass validation against wp_legacy_users.
wp_hashed_password before checking auth_user_id, even for users who had
already been migrated. That meant any user whose Supabase password
diverged from the original WP hash (e.g., curated migration users like
ryan.salazar@relevantmediaproperties.com — wp_id=1 'broadcastbeat',
where the hash on the legacy row is from the migration template, not
the user's current password) would get 401 even with the correct
Supabase password.
Fix: if auth_user_id is set OR password_upgraded is true, skip phpass
and go directly to supabase.auth.signInWithPassword. The legacy hash
is only authoritative for the first-ever login (when neither flag is
set). Returns legacy:true so the client can distinguish from the
fully-native signInWithPassword fallback (legacy:false).
The /api/auth/wp-login route previously returned 401 immediately if the
email had no bb.wp_legacy_users entry. That made it impossible for
modern staff accounts (created directly via the Supabase Auth Admin
API, no WP migration history) to sign in via /client-login.
When the legacy lookup misses, fall through to a regular
supabase.auth.signInWithPassword call. If that succeeds, return a
success response with legacy=false so the client can distinguish
modern auth from a phpass-migrated session.
Existing legacy WP users keep working — the fallback only fires when
the legacy lookup returns no rows.
Reorganize the navigation:
- navLinks now carries an optional dropdown[] per section. NEWS / GEAR &
REVIEWS / SHOW COVERAGE / TECHNOLOGY get sub-items pulled from the old
flat categoryLinks list.
- Desktop main nav renders each top-level item as a hover-triggered
dropdown (CSS-only via Tailwind group-hover + group-focus-within for
keyboard accessibility). aria-haspopup advertises the popup; the panel
is role=menu with menuitem children.
- Mobile drawer's category list now groups by parent section instead of
showing a flat list.
- Removed the secondary category sub-nav row that previously sat below
the main nav (its content is in the dropdowns now).
- Added a site-wide 728x90 ad slot between the top utility bar and the
main nav (md+ only). Uses <AdSlot zone='homepage-top'> so the existing
ad-rotation pool serves it.
Net: 566 → ~600 lines (markup heavier than what was removed because the
dropdown panels are inline).