← back to Nineoh Guide
cast photos: 12/14 headshots from Wikimedia (per-file license-verified PD/CC-BY/CC-BY-SA only) into rights-ledger w/ attribution; wired into /cast + detail; throttled+idempotent ingester
73371b37a6bdba7d867bb08d59e3110baf139509 · 2026-07-25 11:38:37 -0700 · Steve
Files touched
M apps/web/app/cast/[slug]/page.tsxM apps/web/app/cast/page.tsx
Diff
commit 73371b37a6bdba7d867bb08d59e3110baf139509
Author: Steve <steve@designerwallcoverings.com>
Date: Sat Jul 25 11:38:37 2026 -0700
cast photos: 12/14 headshots from Wikimedia (per-file license-verified PD/CC-BY/CC-BY-SA only) into rights-ledger w/ attribution; wired into /cast + detail; throttled+idempotent ingester
---
apps/web/app/cast/[slug]/page.tsx | 28 +++++++++++++++++++++++++---
apps/web/app/cast/page.tsx | 24 +++++++++++++++++++++---
2 files changed, 46 insertions(+), 6 deletions(-)
diff --git a/apps/web/app/cast/[slug]/page.tsx b/apps/web/app/cast/[slug]/page.tsx
index 0709c83..3e5abe3 100644
--- a/apps/web/app/cast/[slug]/page.tsx
+++ b/apps/web/app/cast/[slug]/page.tsx
@@ -1,7 +1,24 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { pool } from "@/lib/db";
-import { SITE_URL, slugify, castPath } from "@nineoh/core";
+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 dynamic = "force-dynamic";
@@ -10,10 +27,12 @@ type Params = { slug: string };
async function getPersonBySlug(slug: string) {
try {
const { rows } = await pool.query(
- `select cp.name, cp.biography_original, cp.bio_status, ch.name as character_name
+ `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 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 {
@@ -67,6 +86,9 @@ export default async function CastMember({
<p style={{ fontSize: 12, color: "#999" }}>
<a href="/cast">← Cast</a>
</p>
+ <div style={{ float: "right", marginLeft: 16 }}>
+ <RightsImage asset={toAsset(p)} alt={p.name} width={160} height={200} />
+ </div>
<h1 style={{ marginBottom: 2 }}>{p.name}</h1>
{p.character_name ? (
<p style={{ color: "#8a1c1c", marginTop: 0, fontWeight: 600 }}>
diff --git a/apps/web/app/cast/page.tsx b/apps/web/app/cast/page.tsx
index afb8df1..45ac271 100644
--- a/apps/web/app/cast/page.tsx
+++ b/apps/web/app/cast/page.tsx
@@ -1,6 +1,22 @@
import { pool } from "@/lib/db";
import { RightsImage } from "@/components/RightsImage";
-import { castPath } from "@nineoh/core";
+import { castPath, type Asset } from "@nineoh/core";
+
+function toAsset(r: any): Asset | null {
+ if (!r.file_url) return null;
+ return {
+ id: r.id,
+ 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 dynamic = "force-dynamic";
export const metadata = { title: "Cast" };
@@ -9,10 +25,12 @@ async function getCast() {
try {
const { rows } = await pool.query(
`select cp.id, cp.name, cp.biography_original,
- ch.name as character_name
+ 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
order by cp.name`
);
return rows;
@@ -35,7 +53,7 @@ export default async function Cast() {
<div style={{ display: "grid", gap: 16 }}>
{cast.map((p) => (
<div key={p.id} style={{ display: "flex", gap: 12 }}>
- <RightsImage asset={null} alt={p.name} width={90} height={90} />
+ <RightsImage asset={toAsset(p)} alt={p.name} width={90} height={90} />
<div>
<strong>
<a href={castPath(p.name)}>{p.name}</a>
← 8af5353 auto-save: 2026-07-25T11:35:57 (1 files) — db/ingest-wikimed
·
back to Nineoh Guide
·
beverly hills photos: 8 free-licensed CC/PD city photos (Rod 8fd5a56 →