[object Object]

← back to Nineoh Guide

cast bios: 14 original ai-draft bios + person->character main-cast links; /cast shows role + bio

9cde2873c501c4687f7d0fe4a5f60fbffb70c321 · 2026-07-25 10:55:38 -0700 · Steve

Files touched

Diff

commit 9cde2873c501c4687f7d0fe4a5f60fbffb70c321
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Jul 25 10:55:38 2026 -0700

    cast bios: 14 original ai-draft bios + person->character main-cast links; /cast shows role + bio
---
 apps/web/app/cast/page.tsx | 12 ++++++++++-
 db/apply-bios.mjs          | 51 ++++++++++++++++++++++++++++++++++++++++++++++
 db/bios.json               | 17 ++++++++++++++++
 3 files changed, 79 insertions(+), 1 deletion(-)

diff --git a/apps/web/app/cast/page.tsx b/apps/web/app/cast/page.tsx
index 0a99afa..9df72b6 100644
--- a/apps/web/app/cast/page.tsx
+++ b/apps/web/app/cast/page.tsx
@@ -7,7 +7,12 @@ export const metadata = { title: "Cast" };
 async function getCast() {
   try {
     const { rows } = await pool.query(
-      `select id, name, biography_original from cast_people order by name`
+      `select cp.id, cp.name, cp.biography_original,
+              ch.name as character_name
+         from cast_people cp
+         left join credits cr on cr.person_id = cp.id and cr.credit_type = 'main-cast'
+         left join characters ch on ch.id = cr.character_id
+        order by cp.name`
     );
     return rows;
   } catch {
@@ -32,6 +37,11 @@ export default async function Cast() {
               <RightsImage asset={null} alt={p.name} width={90} height={90} />
               <div>
                 <strong>{p.name}</strong>
+                {p.character_name ? (
+                  <span style={{ color: "#8a1c1c", fontSize: 13 }}>
+                    {" "}as {p.character_name}
+                  </span>
+                ) : null}
                 <p style={{ margin: "4px 0", color: "#555", fontSize: 13 }}>
                   {p.biography_original ?? "Bio coming soon."}
                 </p>
diff --git a/db/apply-bios.mjs b/db/apply-bios.mjs
new file mode 100644
index 0000000..7a768a2
--- /dev/null
+++ b/db/apply-bios.mjs
@@ -0,0 +1,51 @@
+// Applies ORIGINAL cast bios from db/bios.json → cast_people.biography_original
+// (bio_status='ai-draft'), and links each actor to their character via the credits
+// table (episode_id NULL, credit_type='main-cast'). Run: node db/apply-bios.mjs
+import pg from "pg";
+import { readFileSync } from "node:fs";
+import { fileURLToPath } from "node:url";
+import { dirname, join } from "node:path";
+
+const DATABASE_URL =
+  process.env.DATABASE_URL ?? "postgresql://localhost/nineoh_guide?host=/tmp";
+const pool = new pg.Pool({ connectionString: DATABASE_URL });
+const here = dirname(fileURLToPath(import.meta.url));
+const bios = JSON.parse(readFileSync(join(here, "bios.json"), "utf8"));
+
+const show = (await pool.query(`select id from shows where canonical_title='90210'`)).rows[0];
+if (!show) { console.error("Run db/seed.mjs first."); process.exit(1); }
+
+let bioN = 0, linkN = 0;
+for (const [name, entry] of Object.entries(bios)) {
+  if (name.startsWith("_")) continue;
+  const person = (await pool.query(`select id from cast_people where name=$1`, [name])).rows[0];
+  if (!person) { console.warn(`no cast_people row for ${name} — skipped`); continue; }
+
+  const b = await pool.query(
+    `update cast_people set biography_original=$1, bio_status='ai-draft' where id=$2`,
+    [entry.bio, person.id]
+  );
+  bioN += b.rowCount;
+
+  // Link person -> character (show-level main-cast credit; episode_id NULL).
+  const ch = (
+    await pool.query(`select id from characters where name=$1 and show_id=$2`, [entry.character, show.id])
+  ).rows[0];
+  if (ch) {
+    const link = await pool.query(
+      `insert into credits (person_id, character_id, credit_type)
+         select $1,$2,'main-cast'
+         where not exists (
+           select 1 from credits where person_id=$1 and character_id=$2 and credit_type='main-cast'
+         )`,
+      [person.id, ch.id]
+    );
+    linkN += link.rowCount;
+  }
+}
+
+console.log(`bios written: ${bioN}, main-cast links created: ${linkN}`);
+const t = await pool.query(`select bio_status, count(*) from cast_people group by bio_status order by 1`);
+console.table(t.rows);
+console.log("All bios ai-draft — review before publish.");
+await pool.end();
diff --git a/db/bios.json b/db/bios.json
new file mode 100644
index 0000000..e745957
--- /dev/null
+++ b/db/bios.json
@@ -0,0 +1,17 @@
+{
+  "_note": "ORIGINAL editorial bios in our own words, grounded in verifiable facts (birth year, nationality, role, one well-known credit). AI-drafted — human review before publish. No copied bio text; no photos.",
+  "Shenae Grimes-Beech": { "character": "Annie Wilson", "bio": "Canadian actress (born 1989) who plays series lead Annie Wilson, the Kansas transplant at the heart of the reboot. She was already familiar to teen audiences from her years on the long-running Canadian drama Degrassi: The Next Generation." },
+  "Jessica Stroup": { "character": "Erin Silver", "bio": "American actress (born 1986) who plays Erin 'Silver' Silver across all five seasons. She went on to co-star in network dramas including The Following." },
+  "AnnaLynne McCord": { "character": "Naomi Clark", "bio": "American actress (born 1987) who plays the wealthy, sharp-edged Naomi Clark. Before 90210 she appeared on Nip/Tuck, and has since worked steadily across film and television." },
+  "Jessica Lowndes": { "character": "Adrianna Tate-Duncan", "bio": "Canadian actress and singer-songwriter (born 1988) who plays aspiring performer Adrianna Tate-Duncan — a role that dovetailed with her own off-screen music career." },
+  "Mack Wilds": { "character": "Dixon Wilson", "bio": "American actor and Grammy-nominated musician (born 1989), credited here as Tristan Wilds, who plays Dixon Wilson. He first drew notice as Michael Lee on the acclaimed HBO series The Wire." },
+  "Matt Lanter": { "character": "William 'Liam' Court", "bio": "American actor (born 1983) who plays brooding bad-boy Liam Court. He is also widely known as the voice of Anakin Skywalker in Star Wars: The Clone Wars and later co-starred in the time-travel drama Timeless." },
+  "Michael Steger": { "character": "Navid Shirazi", "bio": "American actor (born 1980) who plays earnest aspiring filmmaker Navid Shirazi, one of the core ensemble across the show's run." },
+  "Lori Loughlin": { "character": "Debbie Wilson", "bio": "American actress (born 1964) who plays family matriarch Debbie Wilson in the early seasons. She was already a household name from her long run as Aunt Becky on Full House." },
+  "Gillian Zinser": { "character": "Ivy Sullivan", "bio": "American actress (born 1985) who plays free-spirited surfer Ivy Sullivan, introduced as the series' ensemble expanded." },
+  "Trevor Donovan": { "character": "Teddy Montgomery", "bio": "American actor and former model (born 1982) who plays Teddy Montgomery, a role tied to one of the show's most talked-about story arcs. He later became a fixture of made-for-television romance films." },
+  "Ryan Eggold": { "character": "Ryan Matthews", "bio": "American actor (born 1984) who plays teacher Ryan Matthews in the early seasons. He went on to lead the network dramas The Blacklist: Redemption and New Amsterdam." },
+  "Rob Estes": { "character": "Harrison 'Harry' Wilson", "bio": "American actor (born 1963) who plays patriarch Harry Wilson. A veteran of 1990s television, he was previously known for Silk Stalkings and Melrose Place." },
+  "Dustin Milligan": { "character": "Ethan Ward", "bio": "Canadian actor (born 1985) who plays good-guy athlete Ethan Ward in the first season. He later won a wider audience as Ted Mullens on the comedy Schitt's Creek." },
+  "Jessica Walter": { "character": "Tabitha Wilson", "bio": "Acclaimed American actress (born 1941) who plays Annie and Dixon's glamorous, quick-witted grandmother Tabitha Wilson. A screen veteran, she was beloved for Arrested Development and the animated series Archer." }
+}

← 2a7ccb0 recaps: complete Season 5 (22 original spoiler-safe drafts)  ·  back to Nineoh Guide  ·  SEO layer: crawlable per-episode + per-cast detail pages w/ e3f69d6 →