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>
This commit is contained in:
Ryan Salazar
2026-05-20 06:04:30 +00:00
parent 6e9c1b59bd
commit 9eadda4b9e
3 changed files with 132 additions and 6 deletions

View File

@@ -0,0 +1,90 @@
import { NextResponse } from "next/server";
import { createClient } from "@supabase/supabase-js";
export const runtime = "nodejs";
export const revalidate = 300;
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const SUPABASE_ANON = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const SCHEMA = process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "bb";
function client() {
return createClient(SUPABASE_URL, SUPABASE_ANON, {
db: { schema: SCHEMA as "public" },
auth: { persistSession: false },
});
}
// Slug → list of author_name values that should match. Multiple entries
// are OR'd so name variants ("Ryan Salazar", "Ryan", etc.) all resolve
// to the same author profile.
const SLUG_TO_NAMES: Record<string, string[]> = {
"ryan-salazar": ["Ryan Salazar"],
"james-whitfield": ["James Whitfield"],
"sarah-chen": ["Sarah Chen"],
"elena-vasquez": ["Elena Vasquez"],
"marcus-rivera": ["Marcus Rivera"],
"rachel-kim": ["Rachel Kim"],
"carlos-mendez": ["Carlos Mendez"],
};
function namesForSlug(slug: string): string[] {
if (SLUG_TO_NAMES[slug]) return SLUG_TO_NAMES[slug];
// Fallback: convert "first-last" → "First Last"
const guess = slug
.split("-")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" ");
return [guess];
}
export async function GET(
req: Request,
{ params }: { params: Promise<{ slug: string }> },
) {
const { slug } = await params;
const { searchParams } = new URL(req.url);
const limit = Math.min(parseInt(searchParams.get("limit") || "500", 10) || 500, 5000);
const names = namesForSlug(slug);
const sb = client();
// Count first (for accurate article-count badge)
const { count: total } = await sb
.from("wp_imported_posts")
.select("wp_id", { count: "exact", head: true })
.eq("status", "published")
.in("author_name", names);
const { data } = await sb
.from("wp_imported_posts")
.select("wp_id, wp_slug, title, excerpt, category, featured_image, wp_published_at, author_name")
.eq("status", "published")
.in("author_name", names)
.order("wp_published_at", { ascending: false, nullsFirst: false })
.limit(limit);
const articles = (data || []).map((r: any) => ({
slug: r.wp_slug,
title: r.title,
excerpt: r.excerpt || "",
category: r.category || "News",
date: r.wp_published_at
? new Date(r.wp_published_at).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })
: "",
image: r.featured_image,
alt: r.title,
readTime: estimateReadTime(r.excerpt || ""),
}));
return NextResponse.json(
{ slug, names, total: total ?? articles.length, articles },
{ headers: { "Cache-Control": "public, s-maxage=300, stale-while-revalidate=600" } },
);
}
function estimateReadTime(text: string): string {
const words = (text || "").trim().split(/\s+/).filter(Boolean).length;
const minutes = Math.max(1, Math.round((words || 200) / 200));
return `${minutes} min read`;
}