diff --git a/system/netlify/functions/crm.mjs b/system/netlify/functions/crm.mjs index 037078f8c7..f2bd509421 100644 --- a/system/netlify/functions/crm.mjs +++ b/system/netlify/functions/crm.mjs @@ -64,7 +64,7 @@ async function rightsFor(database, handle) { const ld = doc.linkedData; if (ld?.enabled === false) return null; const license = isLicense(ld?.license) ? ld.license : DEFAULT_RIGHTS; - return { handle: doc.handle, license }; + return { handle: doc.handle, license, colors: doc.colors || [] }; } // In dev, ?preview=CC-BY-4.0 bypasses the gate so we can validate serialization @@ -89,17 +89,69 @@ function jsonld(doc) { return respond(200, doc, { "Content-Type": JSONLD }); } -// A minimal human view that points at both the JSON-LD and the apex page. -function htmlView(doc, webUrl) { - const body = `${doc._label} - -

${doc._label}

-

Linked Art (CIDOC CRM) identity on data.aesthetic.computer.

+// Render a handle as its AC sigil: each glyph of "@handle" tinted by the +// per-character colors stored in @handles.colors. +function sigil(handle, colors) { + if (!handle) return ""; + const clean = handle.replace(/^@/, ""); + const glyphs = [...`@${clean}`] + .map((ch, i) => { + const c = colors?.[i]; + const col = c ? `rgb(${c.r},${c.g},${c.b})` : "var(--accent)"; + return `${esc(ch)}`; + }) + .join(""); + return `${glyphs}`; +} + +// A rich human view of a record — the handle sigil, the painting image, rights, +// and links to the JSON-LD / IIIF / apex. Styled to match the landing (auto +// light/dark). Machines still get JSON-LD via content negotiation. +function htmlView(doc, webUrl, extras = {}) { + const { kind, handle, colors, imageUrl, code } = extras; + const rights = doc.subject_to?.[0]; + const rightsHtml = rights + ? `

Rights: ${esc(rights._label)}

` + : ""; + const img = imageUrl + ? `${esc(doc._label)}` + : ""; + const quote = doc.type === "LinguisticObject" && doc.content ? `
“${esc(doc.content)}”
` : ""; + const iiif = + kind === "painting" + ? `
  • IIIF manifest · open in Mirador ↗
  • ` + : ""; + const body = ` + +${esc(doc._label)} · data + + + +data +${handle ? `
    ${sigil(handle, colors)}
    ` : ""} +

    ${esc(doc._label)}

    +${img} +${quote} +${rightsHtml} -
    ${JSON.stringify(doc, null, 2).replace(/`;
    +`;
       return respond(200, body, { "Content-Type": "text/html; charset=utf-8" });
     }
     
    @@ -137,7 +189,9 @@ export async function handler(event) {
             .findOne({ user: await subForHandle(database, consent.handle), deleted: { $ne: true } }, { sort: { when: -1 } });
     
           const doc = personToLinkedArt({ handle: consent.handle, latestMood });
    -      return finish(event, doc, `${WEB_BASE}/@${consent.handle}`);
    +      return finish(event, doc, `${WEB_BASE}/@${consent.handle}`, {
    +        kind: "person", handle: consent.handle, colors: consent.colors,
    +      });
         }
     
         // Painting: /painting/{code}
    @@ -145,7 +199,9 @@ export async function handler(event) {
           const pt = await loadPainting(database, segs[1], event);
           if (!pt) return respond(404, { message: "Not found" });
           const doc = paintingToLinkedArt({ code: pt.code, handle: pt.handle, when: pt.when, imageUrl: pt.imageUrl, license: pt.license });
    -      return finish(event, doc, `${WEB_BASE}/painting/${pt.code}`);
    +      return finish(event, doc, `${WEB_BASE}/painting/${pt.code}`, {
    +        kind: "painting", handle: pt.handle, colors: pt.colors, imageUrl: pt.imageUrl, code: pt.code,
    +      });
         }
     
         // IIIF surfaces: /iiif/{code}/manifest | /info.json | /full/max/0/default.png
    @@ -187,7 +243,9 @@ export async function handler(event) {
             hash: piece.hash,
             license: consent.license,
           });
    -      return finish(event, doc, `${WEB_BASE}/${piece.code}`);
    +      return finish(event, doc, `${WEB_BASE}/${piece.code}`, {
    +        kind: "piece", handle, colors: consent.colors, code: piece.code,
    +      });
         }
     
         // Mood: /mood/{handle}/{rkey}
    @@ -211,7 +269,9 @@ export async function handler(event) {
             blueskyUri: mood.bluesky?.uri,
             license: consent.license,
           });
    -      return finish(event, doc, `${WEB_BASE}/moods~${consent.handle}~${rkey}`);
    +      return finish(event, doc, `${WEB_BASE}/moods~${consent.handle}~${rkey}`, {
    +        kind: "mood", handle: consent.handle, colors: consent.colors,
    +      });
         }
     
         return respond(404, { message: "Not found" });
    @@ -245,6 +305,7 @@ async function loadPainting(database, code, event) {
         handle,
         when: p.when,
         license: consent.license,
    +    colors: consent.colors,
         imageUrl: paintingImageUrl(handle, p.slug, p.user),
       };
     }
    @@ -282,8 +343,8 @@ async function sitemap(database) {
       return respond(200, body, { "Content-Type": "application/xml; charset=utf-8" });
     }
     
    -function finish(event, doc, webUrl) {
    -  return wantsHtml(event) ? htmlView(doc, webUrl) : jsonld(doc);
    +function finish(event, doc, webUrl, extras = {}) {
    +  return wantsHtml(event) ? htmlView(doc, webUrl, extras) : jsonld(doc);
     }
     
     function voidDoc() {