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).
This commit is contained in:
Ryan Salazar
2026-05-20 06:25:08 +00:00
parent fa94e92af5
commit f7c462891f
3 changed files with 521 additions and 418 deletions

View File

@@ -0,0 +1,142 @@
-- ============================================================
-- Migration: bb.author_profiles
-- Date: 2026-05-20
-- Purpose: Per-author bio + contact card backing /authors/[slug].
-- Replaces the 540-line static map in src/app/authors/[slug]/page.tsx
-- with a DB-driven profile that works for ALL 160 distinct
-- author_name values in wp_imported_posts (not just the 7 we
-- hand-coded). Join date is computed live from
-- MIN(wp_published_at), never stored, so it stays correct
-- as more legacy posts get backfilled.
-- ============================================================
CREATE TABLE IF NOT EXISTS bb.author_profiles (
slug TEXT PRIMARY KEY, -- url slug: "desert-moon-communications"
display_name TEXT NOT NULL, -- "Desert Moon Communications"
author_names TEXT[] NOT NULL DEFAULT '{}', -- name variants in wp_imported_posts.author_name
role TEXT NOT NULL DEFAULT 'Contributor',-- "Contributor" | "Founder of Broadcast Beat" | "Senior Editor" | ...
kind TEXT NOT NULL DEFAULT 'contributor',-- 'staff' | 'contributor' | 'pr_firm' | 'person'
bio TEXT, -- AI-generated paragraph
bio_generated_at TIMESTAMPTZ, -- when Ollama wrote it
bio_source_model TEXT, -- 'qwen2.5-coder:32b' etc.
-- contact card
address_line1 TEXT,
address_line2 TEXT,
city TEXT,
state TEXT,
postal_code TEXT,
country TEXT,
phone TEXT,
email TEXT,
website TEXT,
-- social
twitter TEXT,
linkedin TEXT,
facebook TEXT,
instagram TEXT,
-- media
avatar_url TEXT,
cover_url TEXT,
-- editorial extras
specialties TEXT[] DEFAULT '{}',
location_text TEXT, -- free-text "Los Angeles, CA" fallback if structured fields absent
meta JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS author_profiles_kind_idx ON bb.author_profiles(kind);
CREATE INDEX IF NOT EXISTS author_profiles_names_gin ON bb.author_profiles USING GIN (author_names);
-- Trigger to keep updated_at fresh
CREATE OR REPLACE FUNCTION bb.author_profiles_touch() RETURNS trigger AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS author_profiles_touch_t ON bb.author_profiles;
CREATE TRIGGER author_profiles_touch_t
BEFORE UPDATE ON bb.author_profiles
FOR EACH ROW EXECUTE FUNCTION bb.author_profiles_touch();
-- Convenience view: join date + post count computed live from imports
CREATE OR REPLACE VIEW bb.author_profiles_with_stats AS
SELECT
p.*,
s.first_post_date,
s.last_post_date,
s.total_articles
FROM bb.author_profiles p
LEFT JOIN LATERAL (
SELECT
MIN(wp_published_at)::date AS first_post_date,
MAX(wp_published_at)::date AS last_post_date,
COUNT(*) AS total_articles
FROM bb.wp_imported_posts
WHERE status = 'published'
AND author_name = ANY(p.author_names)
) s ON true;
-- RLS: read-only to anon, full to service_role
ALTER TABLE bb.author_profiles ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS author_profiles_public_read ON bb.author_profiles;
CREATE POLICY author_profiles_public_read
ON bb.author_profiles FOR SELECT
TO anon, authenticated
USING (true);
DROP POLICY IF EXISTS author_profiles_service_write ON bb.author_profiles;
CREATE POLICY author_profiles_service_write
ON bb.author_profiles FOR ALL
TO service_role
USING (true)
WITH CHECK (true);
-- Seed Ryan immediately so the live site is correct the moment this lands.
INSERT INTO bb.author_profiles (
slug, display_name, author_names, role, kind, bio,
email, website, twitter, linkedin,
location_text, country,
avatar_url, cover_url,
specialties
)
VALUES (
'ryan-salazar',
'Ryan Salazar',
ARRAY['Ryan Salazar'],
'Founder of Broadcast Beat',
'staff',
'Ryan Salazar is the founder of Broadcast Beat Magazine and the Executive Producer of NAB Show LIVE for the National Association of Broadcasters. With more than 20 years in audio, visual, and broadcast technology, Ryan began his career in 1997 as an audio engineer and producer/director at ProComm Studios, then spent seven years as Chief Technology Officer at SunSpots Productions running all AV and IT operations. In 2005 he launched AV Beat to cover the audio-visual industry, and in 2014 transitioned to SMPTE LIVE as Executive Producer and on-camera talent, interviewing industry luminaries including James Cameron, Douglas Trumbull, Billy Zane, and Richard Edlund. Ryan founded Broadcast Beat in December 2013 to give broadcast and production engineers a dedicated trade publication focused on the engineering side of the industry — live production, IP workflows, streaming, cloud, audio, and post. In 2020 he also became President and CTO of Broadcast Beat Studios, a production company based in South Florida. Today he continues to lead Broadcast Beat alongside Relevant Media Properties'' growing portfolio of trade publications.',
'ryan.salazar@relevantmediaproperties.com',
'https://www.relevantmediaproperties.com/',
'https://twitter.com/broadcastbeat',
'https://linkedin.com/company/broadcastbeat',
'South Florida, United States',
'United States',
'/assets/images/logo.png',
'/assets/images/og-image.png',
ARRAY['Broadcast Engineering','Live Production','Industry Strategy','Editorial Direction']
)
ON CONFLICT (slug) DO UPDATE SET
display_name = EXCLUDED.display_name,
author_names = EXCLUDED.author_names,
role = EXCLUDED.role,
kind = EXCLUDED.kind,
bio = EXCLUDED.bio,
email = EXCLUDED.email,
website = EXCLUDED.website,
twitter = EXCLUDED.twitter,
linkedin = EXCLUDED.linkedin,
location_text = EXCLUDED.location_text,
country = EXCLUDED.country,
avatar_url = EXCLUDED.avatar_url,
cover_url = EXCLUDED.cover_url,
specialties = EXCLUDED.specialties;