-- ============================================================ -- 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;