← back to Nineoh Guide
apps/web/app/cast/[slug]/page.tsx
104 lines
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { pool } from "@/lib/db";
import { SITE_URL, slugify, castPath, type Asset } from "@nineoh/core";
import { RightsImage } from "@/components/RightsImage";
function toAsset(r: any): Asset | null {
if (!r?.file_url) return null;
return {
id: r.id ?? "x",
type: "image",
fileUrl: r.file_url,
thumbnailUrl: r.file_url,
licenseType: r.license_type,
licenseUrl: r.license_url ?? null,
attributionText: r.attribution_text ?? null,
usageScope: "editorial",
approvedForMarketing: false,
licensePurchaseStatus: "none",
};
}
export const revalidate = 3600; // ISR: cached HTML, hourly background refresh
type Params = { slug: string };
async function getPersonBySlug(slug: string) {
try {
const { rows } = await pool.query(
`select cp.id, cp.name, cp.biography_original, cp.bio_status, ch.name as character_name,
a.file_url, a.license_type, a.license_url, a.attribution_text
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
left join assets a on a.id = cp.headshot_asset_id`
);
return rows.find((r) => slugify(r.name) === slug) ?? null;
} catch {
return null;
}
}
export async function generateMetadata({
params,
}: {
params: Promise<Params>;
}): Promise<Metadata> {
const { slug } = await params;
const p = await getPersonBySlug(slug);
if (!p) return { title: "Cast member not found" };
const t = p.character_name ? `${p.name} — ${p.character_name}` : p.name;
const desc = p.biography_original ?? `${p.name} in the 90210 cast.`;
return {
title: t,
description: desc,
alternates: { canonical: `${SITE_URL}${castPath(p.name)}` },
openGraph: { title: t, description: desc, type: "profile" },
};
}
export default async function CastMember({
params,
}: {
params: Promise<Params>;
}) {
const { slug } = await params;
const p = await getPersonBySlug(slug);
if (!p) notFound();
const jsonLd = {
"@context": "https://schema.org",
"@type": "Person",
name: p.name,
...(p.biography_original ? { description: p.biography_original } : {}),
...(p.character_name
? { performerIn: { "@type": "TVSeries", name: "90210 (2008)" } }
: {}),
};
return (
<article style={{ lineHeight: 1.7 }}>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<p style={{ fontSize: 12, color: "var(--muted)" }}>
<a href="/cast">← Cast</a>
</p>
<h1 style={{ marginBottom: 2 }}>{p.name}</h1>
{p.character_name ? (
<p style={{ color: "var(--maroon)", marginTop: 0, fontWeight: 600 }}>
as {p.character_name}
</p>
) : null}
<p style={{ fontSize: 15 }}>
{p.biography_original ?? "An original bio for this cast member is coming soon."}
</p>
{p.bio_status === "ai-draft" ? (
<p style={{ fontSize: 11, color: "var(--muted)" }}>Editorial bio draft — pending review.</p>
) : null}
</article>
);
}