[object Object]

← back to CelebritySignatures

Museum expansion wiring: +10 museums in P195 harvest (LACMA, The Broad, MOCA, Getty, SFMOMA, Whitney, Guggenheim, Cleveland, MFA Boston, Philadelphia; QIDs verified), union-not-overwrite artists-raw writes; popup works loader now multi-source (Met -> AIC -> Cleveland, all keyless CORS) with per-tile museum label (TK-10181)

a7c40c4a75d737da96611bda4c50bcfa4377c9f3 · 2026-08-03 11:22:16 -0700 · Steve Abrams

Files touched

Diff

commit a7c40c4a75d737da96611bda4c50bcfa4377c9f3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 3 11:22:16 2026 -0700

    Museum expansion wiring: +10 museums in P195 harvest (LACMA, The Broad, MOCA, Getty, SFMOMA, Whitney, Guggenheim, Cleveland, MFA Boston, Philadelphia; QIDs verified), union-not-overwrite artists-raw writes; popup works loader now multi-source (Met -> AIC -> Cleveland, all keyless CORS) with per-tile museum label (TK-10181)
---
 public/index.html         | 49 ++++++++++++++++++++++++++++++++++-------------
 scripts/fetch-artists.mjs | 35 ++++++++++++++++++++++++++++++---
 2 files changed, 68 insertions(+), 16 deletions(-)

diff --git a/public/index.html b/public/index.html
index 290dd0d..b41bc20 100644
--- a/public/index.html
+++ b/public/index.html
@@ -221,25 +221,48 @@ function evoStripHTML(qid){
     e.sigs.map(s=>`<div class="evo-cell"><img loading="lazy" src="${esc(s.url)}" alt="Signature of ${esc(e.name)}${s.year?' ('+s.year+')':''}" onerror="this.closest('.evo-cell').remove()"><div class="y">${s.year||'undated'}</div></div>`).join('')
   }</div>`;
 }
+// Works loader — free open-museum APIs, tried in order until we have 6:
+// The Met (Open Access) → Art Institute of Chicago (PD only) → Cleveland
+// Museum of Art. All keyless + CORS-open; each tile links to the museum page.
 async function loadWorks(qid, name){
   if (WORKS_CACHE[qid]) return WORKS_CACHE[qid];
-  const MET='https://collectionapi.metmuseum.org/public/collection/v1';
+  const works = [];
+  const last = name.toLowerCase().split(' ').pop();
+  const MAX = 6;
+  // 1) The Met
   try {
+    const MET='https://collectionapi.metmuseum.org/public/collection/v1';
     const s = await (await fetch(`${MET}/search?hasImages=true&artistOrCulture=true&q=${encodeURIComponent(name)}`)).json();
-    const ids = (s.objectIDs||[]).slice(0,14);
-    const nl = name.toLowerCase();
-    const works = [];
-    for (const id of ids) {
-      if (works.length >= 6) break;
+    for (const id of (s.objectIDs||[]).slice(0,14)) {
+      if (works.length >= MAX) break;
       try {
         const o = await (await fetch(`${MET}/objects/${id}`)).json();
         if (!o.primaryImageSmall) continue;
-        if (!(o.artistDisplayName||'').toLowerCase().includes(nl.split(' ').pop())) continue;
-        works.push({ img:o.primaryImageSmall, title:o.title||'Untitled', date:o.objectDate||'', url:o.objectURL||('https://www.metmuseum.org/art/collection/search/'+id) });
+        if (!(o.artistDisplayName||'').toLowerCase().includes(last)) continue;
+        works.push({ img:o.primaryImageSmall, title:o.title||'Untitled', date:o.objectDate||'', url:o.objectURL||('https://www.metmuseum.org/art/collection/search/'+id), src:'The Met' });
       } catch {}
     }
-    WORKS_CACHE[qid]=works; return works;
-  } catch { return []; }
+  } catch {}
+  // 2) Art Institute of Chicago
+  if (works.length < MAX) try {
+    const j = await (await fetch(`https://api.artic.edu/api/v1/artworks/search?q=${encodeURIComponent(name)}&query[term][is_public_domain]=true&fields=id,title,image_id,artist_title,date_display&limit=12`)).json();
+    for (const a of (j.data||[])) {
+      if (works.length >= MAX) break;
+      if (!a.image_id || !(a.artist_title||'').toLowerCase().includes(last)) continue;
+      works.push({ img:`https://www.artic.edu/iiif/2/${a.image_id}/full/400,/0/default.jpg`, title:a.title||'Untitled', date:a.date_display||'', url:`https://www.artic.edu/artworks/${a.id}`, src:'Art Institute of Chicago' });
+    }
+  } catch {}
+  // 3) Cleveland Museum of Art
+  if (works.length < MAX) try {
+    const j = await (await fetch(`https://openaccess-api.clevelandart.org/api/artworks/?artists=${encodeURIComponent(name)}&has_image=1&limit=10`)).json();
+    for (const a of (j.data||[])) {
+      if (works.length >= MAX) break;
+      const img = a.images?.web?.url; if (!img) continue;
+      if (!(a.creators||[]).some(c=>(c.description||'').toLowerCase().includes(last))) continue;
+      works.push({ img, title:a.title||'Untitled', date:a.creation_date||'', url:a.url||'https://www.clevelandart.org/art/collection/search', src:'Cleveland Museum of Art' });
+    }
+  } catch {}
+  WORKS_CACHE[qid]=works; return works;
 }
 function openArtist(qid, push=true){
   const r = BY_QID[qid] || DATA.find(x=>(x.wikidata||'').endsWith('/'+qid));
@@ -255,15 +278,15 @@ function openArtist(qid, push=true){
     <div class="ap-sec">License & sources</div>
     <div class="ap-sub">${esc(r.image_license||'')} · <a href="${esc(r.wikidata)}" target="_blank" rel="noopener noreferrer">Wikidata</a> · <a href="${esc(r.backup_source)}" target="_blank" rel="noopener noreferrer">signature source</a></div>
     ${evoStripHTML(qid)}
-    <div class="ap-sec">Works — The Met Open Access</div>
+    <div class="ap-sec">Works — open museum collections</div>
     <div class="works" id="apWorks"><span class="ap-sub">loading works…</span></div>`;
   $('#artistPop').hidden = false;
   document.body.style.overflow = 'hidden';
   if (push) { const u=new URLSearchParams(location.search); u.set('artist',qid); history.pushState({},'', '?'+u.toString()); }
   loadWorks(qid, r.full_name).then(ws=>{
     const el=$('#apWorks'); if(!el) return;
-    el.innerHTML = ws.length ? ws.map(w=>`<a href="${esc(w.url)}" target="_blank" rel="noopener noreferrer"><img loading="lazy" src="${esc(w.img)}" alt="${esc(w.title)}"><span class="wt">${esc(w.title)}${w.date?' · '+esc(w.date):''}</span></a>`).join('')
-      : `<span class="ap-sub">no openly-licensed works found in the Met's collection — <a href="${esc(r.wikidata)}" target="_blank" rel="noopener noreferrer">see Wikidata</a></span>`;
+    el.innerHTML = ws.length ? ws.map(w=>`<a href="${esc(w.url)}" target="_blank" rel="noopener noreferrer"><img loading="lazy" src="${esc(w.img)}" alt="${esc(w.title)}"><span class="wt">${esc(w.title)}${w.date?' · '+esc(w.date):''}${w.src?' · '+esc(w.src):''}</span></a>`).join('')
+      : `<span class="ap-sub">no openly-licensed works found in the open museum APIs — <a href="${esc(r.wikidata)}" target="_blank" rel="noopener noreferrer">see Wikidata</a></span>`;
   });
 }
 function closeArtist(push=true){
diff --git a/scripts/fetch-artists.mjs b/scripts/fetch-artists.mjs
index fac3311..fee9e35 100644
--- a/scripts/fetch-artists.mjs
+++ b/scripts/fetch-artists.mjs
@@ -9,7 +9,7 @@
 // the catalog's existing deceased-only rule.
 // "Famous first" = ORDER BY DESC(sitelinks) — Wikipedia language-edition count.
 
-import { writeFile } from 'node:fs/promises';
+import { writeFile, readFile } from 'node:fs/promises';
 import { join } from 'node:path';
 import { fileURLToPath } from 'node:url';
 
@@ -33,6 +33,19 @@ const MUSEUMS = {
   Q190804: 'Rijksmuseum',
   Q51252: 'Uffizi Gallery',
   Q160112: 'Museo del Prado',
+  // West-coast + open-collection expansion (Steve 2026-08-03: "all public
+  // museums — the Broad, MOCA, LACMA… keep building"). QIDs verified via
+  // wbsearchentities same day.
+  Q1641836: 'Los Angeles County Museum of Art',
+  Q17060100: 'The Broad',
+  Q1134646: 'Museum of Contemporary Art, Los Angeles',
+  Q731126: 'J. Paul Getty Museum',
+  Q913672: 'San Francisco Museum of Modern Art',
+  Q639791: 'Whitney Museum of American Art',
+  Q201469: 'Solomon R. Guggenheim Museum',
+  Q657415: 'Cleveland Museum of Art',
+  Q49133: 'Museum of Fine Arts Boston',
+  Q510324: 'Philadelphia Museum of Art',
 };
 const VALUES = Object.keys(MUSEUMS).map(q => `wd:${q}`).join(' ');
 
@@ -96,5 +109,21 @@ console.log('top 15 by fame:');
 artists.slice(0, 15).forEach((a, i) =>
   console.log(`  ${i + 1}. ${a.full_name} — ${a.sitelinks} wikis, d.${a.death_year}, ${a.museums.length} museums`));
 
-await writeFile(join(ROOT, 'data', 'artists-raw.json'), JSON.stringify(artists, null, 2));
-console.log(`\nwrote data/artists-raw.json (${artists.length} artists)`);
+// UNION into the existing raw set (never overwrite — the roster gap-fill
+// harvester also feeds artists-raw.json). Idempotent by QID; museum
+// provenance merges; fresher sitelink/occupation data wins.
+const rawPath = join(ROOT, 'data', 'artists-raw.json');
+let existing = [];
+try { existing = JSON.parse(await readFile(rawPath, 'utf8')); } catch { /* first run */ }
+const byQid = new Map(existing.map((a) => [a.qid, a]));
+let added = 0, updated = 0;
+for (const a of artists) {
+  const cur = byQid.get(a.qid);
+  if (!cur) { byQid.set(a.qid, a); added++; continue; }
+  for (const m of a.museums) if (!cur.museums.includes(m)) { cur.museums.push(m); updated++; }
+  if (a.occupations.length > (cur.occupations || []).length) cur.occupations = a.occupations;
+  if (a.sitelinks > (cur.sitelinks || 0)) cur.sitelinks = a.sitelinks;
+}
+const unioned = [...byQid.values()].sort((x, y) => (y.sitelinks || 0) - (x.sitelinks || 0));
+await writeFile(rawPath, JSON.stringify(unioned, null, 2));
+console.log(`\nartists-raw.json: ${existing.length} -> ${unioned.length} (+${added} new, ${updated} museum-tag merges)`);

← faaabcc Minimal cards (signature + name only) + artist details popup  ·  back to CelebritySignatures  ·  Artists mega-collage (895 verified signatures, 10x90 wall + 7318273 →