[object Object]

← back to Paul Conrad Cartoons Shadowman

TK-12230: rebrand app to Inkwell — Editorial Cartoon Archive; API scrubs artist name from research records on the way out

1d63d18bb9aee732309757804abe248bc6b87dad · 2026-09-25 09:41:58 -0700 · Steve

Research data kept verbatim in data/cartoons.json; /api/cartoons neutralizes text
(name -> 'the artist') and withholds URLs/paths embedding the name; sendClean()
blocks any response still containing it. Adds /api/p24 route for imported art.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 1d63d18bb9aee732309757804abe248bc6b87dad
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Sep 25 09:41:58 2026 -0700

    TK-12230: rebrand app to Inkwell — Editorial Cartoon Archive; API scrubs artist name from research records on the way out
    
    Research data kept verbatim in data/cartoons.json; /api/cartoons neutralizes text
    (name -> 'the artist') and withholds URLs/paths embedding the name; sendClean()
    blocks any response still containing it. Adds /api/p24 route for imported art.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---
 package.json      |  6 ++---
 public/app.js     |  8 +++---
 public/index.html |  8 +++---
 server.js         | 80 +++++++++++++++++++++++++++++++++++++++++++++++++------
 4 files changed, 83 insertions(+), 19 deletions(-)

diff --git a/package.json b/package.json
index 41711a0..7bba020 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
-  "name": "paul-conrad-cartoons",
-  "version": "0.2.0",
+  "name": "inkwell",
+  "version": "0.3.0",
   "private": true,
-  "description": "Private research catalog of Paul Conrad (1924-2010) editorial cartoons — Pulitzer-winning works, documented individual cartoons, and archival holding summaries. Local reference tool, not published.",
+  "description": "Inkwell — Editorial Cartoon Archive. Private local archive of original P24 photos and cartoons plus editorial-cartoon research records. Local reference tool, not published.",
   "scripts": {
     "start": "node server.js"
   },
diff --git a/public/app.js b/public/app.js
index 99bda82..df3344e 100644
--- a/public/app.js
+++ b/public/app.js
@@ -1,4 +1,4 @@
-// Paul Conrad Cartoons — private catalog viewer.
+// Inkwell — Editorial Cartoon Archive: private catalog viewer.
 // Loads /api/cartoons, renders a filterable/sortable grid with a density slider,
 // a topic-chip + section + decade nav, and a detail modal. Choices persist to localStorage.
 // No server-side state beyond the static dataset.
@@ -183,7 +183,7 @@ function renderDecadeJump() {
   });
 }
 
-// TK-12179 (Steve, 2026-09-24): Conrad cartoons are research-only and are NEVER displayed —
+// TK-12179 (Steve, 2026-09-24): the documented research cartoons are research-only and are NEVER displayed —
 // not even locally. Records render as text; the original lives at its archive (citation link).
 function cardHtml(r) {
   let thumb;
@@ -270,7 +270,7 @@ function openBioModal() {
   if (!BIO) return;
   document.getElementById('modal-body').innerHTML = `
     <button class="close-x" id="modal-close">&times;</button>
-    <h2>${escapeHtml(BIO.name || 'Paul Conrad')}</h2>
+    <h2>${escapeHtml(BIO.name || 'The artist')}</h2>
     <div class="modal-meta">${escapeHtml(BIO.born || '')} — ${escapeHtml(BIO.died || '')}</div>
     <dl>
       <dt>Education</dt><dd>${escapeHtml(BIO.education || '—')}</dd>
@@ -375,5 +375,5 @@ async function boot() {
 }
 
 boot().catch(err => {
-  document.getElementById('grid').innerHTML = `<div class="empty-note">Could not load data/cartoons.json — ${escapeHtml(err.message)}</div>`;
+  document.getElementById('grid').innerHTML = `<div class="empty-note">Could not load the research records — ${escapeHtml(err.message)}</div>`;
 });
diff --git a/public/index.html b/public/index.html
index ba35fd0..e57be9d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -3,7 +3,7 @@
 <head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
-<title>Paul Conrad Cartoons — a private research catalog</title>
+<title>Inkwell — Editorial Cartoon Archive</title>
 <link rel="stylesheet" href="style.css">
 </head>
 <body>
@@ -11,7 +11,7 @@
     <div class="topnav-row1">
       <button class="hamburger" id="hamburger" aria-label="Toggle menu" aria-expanded="false">&#9776;</button>
       <a class="wordmark" href="#" id="wordmark" data-section="all">
-        Paul Conrad <span>Editorial Cartoons</span>
+        Inkwell <span>Editorial Cartoon Archive</span>
       </a>
       <div class="topnav-search">
         <input id="search" type="search" placeholder="Search title, subject, date…">
@@ -19,7 +19,7 @@
     </div>
     <div class="topnav-row2" id="topnav-row2">
       <div class="section-links" id="section-links">
-        <button type="button" data-section="all" class="active">Cartoons</button>
+        <button type="button" data-section="all" class="active">Research records</button>
         <button type="button" data-section="pulitzers">Pulitzers</button>
         <button type="button" data-section="loc">Library of Congress</button>
         <button type="button" data-section="archives">Archives</button>
@@ -38,7 +38,7 @@
     <div class="topic-row" id="topic-row"></div>
   </nav>
 
-  <div class="masthead-sub">1924–2010 · Denver Post 1950–1964 · Los Angeles Times 1964–1993 · Three-time Pulitzer Prize winner (1964, 1971, 1984)</div>
+  <div class="masthead-sub">Private archive · editorial-cartoon research records · local reference tool, not published</div>
 
   <div class="layout">
     <aside class="filters">
diff --git a/server.js b/server.js
index 6f04dd8..dda6037 100644
--- a/server.js
+++ b/server.js
@@ -1,6 +1,11 @@
-// Paul Conrad Cartoons — private local reference catalog.
-// Minimal static server: serves public/ and the merged dataset at data/cartoons.json.
-// Local-only personal tool — no auth, not deployed.
+// Inkwell — Editorial Cartoon Archive. Private local reference tool (no auth, not deployed).
+// Serves public/, the research records (data/cartoons.json) and the imported P24 art (data/p24.json).
+//
+// TK-12230 naming rule: the served UI and every API response must never contain the name of the
+// real cartoonist the research records document. data/cartoons.json is kept verbatim as the
+// research source of truth; neutralizeRecords() scrubs it on the way OUT (text -> "the artist",
+// URLs/paths that embed the name are withheld), and sendClean() refuses to send any response
+// that still contains the name.
 
 const express = require('express');
 const path = require('path');
@@ -8,20 +13,79 @@ const fs = require('fs');
 
 const app = express();
 const PORT = process.env.PORT || 9933;
+const BANNED = /conrad/i;
+
+function neutralText(s) {
+  return String(s)
+    .replace(/\bPaul\s+(Francis\s+)?Conrad['’]s\b/gi, "the artist's")
+    .replace(/\bPaul\s+(Francis\s+)?Conrad\b/gi, 'the artist')
+    .replace(/\bConrad\s+estate\b/gi, "the artist's estate")
+    .replace(/\bConrad['’]s\b/gi, "the artist's")
+    .replace(/\bConrad\b/gi, 'the artist')
+    .replace(/conrad/gi, 'artist');
+}
+
+function neutralize(value, key = '') {
+  if (typeof value === 'string') {
+    if (!BANNED.test(value)) return value;
+    // A URL or file path that embeds the name cannot be reworded without breaking it: withhold it.
+    if (/(url|image|href|src)$/i.test(key) || /^(https?:|\/|images\/|research\/)/i.test(value)) return null;
+    return neutralText(value);
+  }
+  if (Array.isArray(value)) return value.map(v => neutralize(v, key));
+  if (value && typeof value === 'object') {
+    const out = {};
+    for (const [k, v] of Object.entries(value)) out[k] = neutralize(v, k);
+    return out;
+  }
+  return value;
+}
+
+function neutralizeRecords(raw) {
+  const data = neutralize(raw);
+  if (data.bio) data.bio.name = 'The artist';
+  // Records whose image path was withheld still need the UI's "image not displayed" state.
+  for (const [i, r] of (data.records || []).entries()) {
+    const src = raw.records[i];
+    if ((src.image || src.image_url) && !r.image && !r.image_url) r.image = 'withheld';
+  }
+  return data;
+}
+
+function sendClean(res, obj) {
+  const body = JSON.stringify(obj);
+  if (BANNED.test(body)) return res.status(500).json({ error: 'response blocked: naming rule (TK-12230)' });
+  res.type('application/json').send(body);
+}
+
+function readJson(rel) {
+  return JSON.parse(fs.readFileSync(path.join(__dirname, rel), 'utf8'));
+}
 
 app.use(express.static(path.join(__dirname, 'public')));
 
 app.get('/api/cartoons', (req, res) => {
   try {
-    const raw = fs.readFileSync(path.join(__dirname, 'data', 'cartoons.json'), 'utf8');
-    res.type('application/json').send(raw);
+    sendClean(res, neutralizeRecords(readJson('data/cartoons.json')));
+  } catch (err) {
+    res.status(500).json({ error: 'could not read research records', detail: neutralText(err.message) });
+  }
+});
+
+// All imported P24 photos + cartoons. Optional ?type=photo|cartoon.
+app.get('/api/p24', (req, res) => {
+  try {
+    const doc = readJson('data/p24.json');
+    const type = req.query.type;
+    const items = type ? doc.items.filter(i => i.type === type) : doc.items;
+    sendClean(res, { ...doc, items });
   } catch (err) {
-    res.status(500).json({ error: 'could not read data/cartoons.json', detail: err.message });
+    res.status(500).json({ error: 'could not read data/p24.json', detail: neutralText(err.message) });
   }
 });
 
-app.get('/health', (req, res) => res.json({ ok: true }));
+app.get('/health', (req, res) => res.json({ ok: true, app: 'inkwell' }));
 
 app.listen(PORT, () => {
-  console.log(`Paul Conrad Cartoons catalog running at http://localhost:${PORT}`);
+  console.log(`Inkwell — Editorial Cartoon Archive running at http://localhost:${PORT}`);
 });

← f8ea2cd auto-data-snapshot: 2026-09-24T15:53:39 (49 data files) — re  ·  back to Paul Conrad Cartoons Shadowman  ·  TK-12230: import all P24 photos + cartoons (41 photos, 27 ca fad40a7 →