[object Object]

← back to Nineoh Guide

beverly hills photos: 8 free-licensed CC/PD city photos (Rodeo Drive, the sign) into rights-ledger; home 'Beverly Hills backdrop' strip w/ attribution

8fd5a564a6bd86067cda6e527352c3108f82ad25 · 2026-07-25 11:40:26 -0700 · Steve

Files touched

Diff

commit 8fd5a564a6bd86067cda6e527352c3108f82ad25
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Jul 25 11:40:26 2026 -0700

    beverly hills photos: 8 free-licensed CC/PD city photos (Rodeo Drive, the sign) into rights-ledger; home 'Beverly Hills backdrop' strip w/ attribution
---
 apps/web/app/page.tsx             | 60 ++++++++++++++++++++++++++
 db/ingest-beverlyhills-photos.mjs | 90 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 150 insertions(+)

diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx
index c5f5662..b6d48cc 100644
--- a/apps/web/app/page.tsx
+++ b/apps/web/app/page.tsx
@@ -24,8 +24,22 @@ async function getStats() {
   }
 }
 
+async function getScenery() {
+  try {
+    const { rows } = await pool.query(
+      `select file_url, caption, attribution_text
+         from assets where rights_notes = 'beverly-hills-location'
+         order by created_at limit 6`
+    );
+    return rows;
+  } catch {
+    return [];
+  }
+}
+
 export default async function Home() {
   const { episodes, cast, recapped } = await getStats();
+  const scenery = await getScenery();
   const jsonLd = {
     "@context": "https://schema.org",
     "@type": "TVSeries",
@@ -68,6 +82,52 @@ export default async function Home() {
         <NavCard href="/search" title="Search" sub="Find any episode, character, or moment" />
       </div>
 
+      {scenery.length > 0 && (
+        <>
+          <h2>The Beverly Hills backdrop</h2>
+          <p style={{ color: "var(--muted)", fontSize: 13, marginTop: -4 }}>
+            The real 90210 — photographed by the Wikimedia Commons community, used
+            under free licenses.
+          </p>
+          <div
+            style={{
+              display: "grid",
+              gap: 10,
+              gridTemplateColumns: "repeat(auto-fill, minmax(190px, 1fr))",
+              marginTop: 12,
+            }}
+          >
+            {scenery.map((s) => (
+              <figure
+                key={s.file_url}
+                style={{
+                  margin: 0,
+                  borderRadius: "var(--radius)",
+                  overflow: "hidden",
+                  border: "1px solid var(--line)",
+                  boxShadow: "var(--shadow)",
+                  background: "var(--paper-2)",
+                }}
+              >
+                {/* eslint-disable-next-line @next/next/no-img-element */}
+                <img
+                  src={s.file_url}
+                  alt={s.caption ?? "Beverly Hills"}
+                  loading="lazy"
+                  style={{ width: "100%", height: 130, objectFit: "cover", display: "block" }}
+                />
+                <figcaption style={{ padding: "6px 8px" }}>
+                  <div style={{ fontSize: 12, fontWeight: 600 }}>{s.caption}</div>
+                  <div style={{ fontSize: 10, color: "var(--muted)" }}>
+                    {s.attribution_text}
+                  </div>
+                </figcaption>
+              </figure>
+            ))}
+          </div>
+        </>
+      )}
+
       {episodes === 0 && (
         <p style={{ marginTop: 20, color: "var(--maroon)", fontSize: 13 }}>
           Content is being curated — recaps and profiles will populate shortly.
diff --git a/db/ingest-beverlyhills-photos.mjs b/db/ingest-beverlyhills-photos.mjs
new file mode 100644
index 0000000..eba2493
--- /dev/null
+++ b/db/ingest-beverlyhills-photos.mjs
@@ -0,0 +1,90 @@
+// Pulls atmospheric photos of BEVERLY HILLS (the city — Rodeo Drive, the sign,
+// palm streets), NOT the show. License-verified free-for-reuse only (PD/CC0/CC-BY/
+// CC-BY-SA). Stored in assets tagged 'beverly-hills-location' with attribution.
+// Run: node db/ingest-beverlyhills-photos.mjs
+import pg from "pg";
+
+const DATABASE_URL =
+  process.env.DATABASE_URL ?? "postgresql://localhost/nineoh_guide?host=/tmp";
+const pool = new pg.Pool({ connectionString: DATABASE_URL });
+const UA = "nineoh-guide/0.1 (unofficial fan project; contact: set-at-launch)";
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+const stripHtml = (s) => (s || "").replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
+
+const FREE = /^(cc0|cc-by-\d|cc-by-sa-\d|pd|public domain)/i;
+const NONFREE = /(nc|nd|non-free|fair use)/i;
+
+async function fetchJson(url, tries = 4) {
+  for (let i = 0; i < tries; i++) {
+    try {
+      const r = await fetch(url, { headers: { "User-Agent": UA } });
+      return JSON.parse(await r.text());
+    } catch {
+      await sleep(800 * (i + 1));
+    }
+  }
+  throw new Error("rate-limited");
+}
+
+const TARGET = 8;
+const SEARCHES = [
+  "Rodeo Drive Beverly Hills",
+  "Beverly Hills sign California",
+  "Beverly Hills City Hall",
+  "Beverly Hills palm trees street",
+  "Beverly Hills California",
+];
+
+let stored = 0;
+for (const q of SEARCHES) {
+  if (stored >= TARGET) break;
+  const u =
+    `https://commons.wikimedia.org/w/api.php?action=query&format=json&generator=search` +
+    `&gsrsearch=${encodeURIComponent(q)}&gsrnamespace=6&gsrlimit=8` +
+    `&prop=imageinfo&iiprop=extmetadata|url&iiurlwidth=900`;
+  let j;
+  try {
+    j = await fetchJson(u);
+  } catch {
+    continue;
+  }
+  const pages = Object.values(j.query?.pages ?? {});
+  for (const page of pages) {
+    if (stored >= TARGET) break;
+    const info = page.imageinfo?.[0];
+    if (!info) continue;
+    const m = info.extmetadata ?? {};
+    const licName = m.LicenseShortName?.value || "";
+    const lic = (m.License?.value || "").toLowerCase();
+    const free = FREE.test(lic) || /public domain|cc0|cc by(?!-nc|-nd)/i.test(licName);
+    if (!free || NONFREE.test(lic) || /nc|nd|fair use/i.test(licName)) continue;
+    // Skip maps/logos/diagrams — want photos.
+    const title = (page.title || "").replace(/^File:/, "");
+    if (/\.svg$|map|logo|seal|diagram|\.pdf$/i.test(title)) continue;
+
+    const url = info.thumburl || info.url;
+    const dup = await pool.query(`select 1 from assets where file_url=$1`, [url]);
+    if (dup.rowCount) continue;
+
+    const licType = /cc0|public domain|^pd/i.test(lic) ? "public-domain" : /sa/i.test(lic) ? "cc-by-sa" : "cc-by";
+    await pool.query(
+      `insert into assets (type, file_url, thumbnail_url, license_type, license_url,
+                           attribution_text, usage_scope, source_url, rights_notes, caption, approved_for_marketing)
+       values ('image',$1,$1,$2,$3,$4,'editorial',$5,'beverly-hills-location',$6,false)`,
+      [
+        url,
+        licType,
+        m.LicenseUrl?.value || null,
+        `${stripHtml(m.Artist?.value || m.Credit?.value || "Unknown")} / ${licName} (via Wikimedia Commons)`,
+        `https://commons.wikimedia.org/wiki/${encodeURIComponent(page.title)}`,
+        title.replace(/\.[a-z]+$/i, "").replace(/_/g, " ").slice(0, 80),
+      ]
+    );
+    stored++;
+    console.log(`OK  ${licType.padEnd(13)} ${title.slice(0, 60)}`);
+  }
+  await sleep(700);
+}
+
+console.log(`\nstored ${stored} Beverly Hills location photos (free-licensed).`);
+await pool.end();

← 73371b3 cast photos: 12/14 headshots from Wikimedia (per-file licens  ·  back to Nineoh Guide  ·  mobile forward: easter eggs — tap UNOFFICIAL badge 5x for hi c7a0053 →