[object Object]

← back to Fantasea Consulting

slideshow: sync sanitized deck; render verified 13 slides

740eb8276a9b35abc94bca33f9a383b79b49e6af · 2026-07-26 08:52:49 -0700 · Steve Abrams

Files touched

Diff

commit 740eb8276a9b35abc94bca33f9a383b79b49e6af
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jul 26 08:52:49 2026 -0700

    slideshow: sync sanitized deck; render verified 13 slides
---
 public/slideshow.html | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/public/slideshow.html b/public/slideshow.html
index 8b22147..0be5f04 100644
--- a/public/slideshow.html
+++ b/public/slideshow.html
@@ -53,6 +53,22 @@
   var deck=document.getElementById('deck'), slides=[], idx=0;
   function esc(s){var d=document.createElement('div');d.textContent=s==null?'':String(s);return d.innerHTML;}
   function txt(el){return el?el.textContent.replace(/\s+/g,' ').trim():'';}
+  // Cover heads keep the deliverable's <br>/<span> styling but are whitelist-sanitized:
+  // never render raw innerHTML from a parsed deliverable (XSS-safe). Only text, <br>, <span>.
+  function sanitizeHead(el){
+    var out='', ns=el.childNodes, i;
+    for(i=0;i<ns.length;i++){
+      var n=ns[i];
+      if(n.nodeType===3){ out+=esc(n.nodeValue); }
+      else if(n.nodeType===1){
+        var t=n.tagName.toLowerCase();
+        if(t==='br'){ out+='<br>'; }
+        else if(t==='span'){ out+='<span>'+esc(n.textContent)+'</span>'; }
+        else { out+=esc(n.textContent); }
+      }
+    }
+    return out;
+  }
 
   // The deliverable lives at /portal on template-based portals, or at / on the
   // originals (car-wash, fantasea). Try /portal, fall back to /.
@@ -65,7 +81,7 @@
     // Cover slide from the deliverable's hero (h1 + first hero paragraph) or title.
     var h1=doc.querySelector('.hero h1, h1');
     var kick=doc.querySelector('.hero .kick, .kick, .eyebrow');
-    out.push({cover:true, kicker:txt(kick)||'Consulting Deliverable', head:(h1?h1.innerHTML:document.title), sub:txt(doc.querySelector('.hero p'))});
+    out.push({cover:true, kicker:txt(kick)||'Consulting Deliverable', head:(h1?sanitizeHead(h1):esc(document.title)), sub:txt(doc.querySelector('.hero p'))});
     // One slide per section.
     var secs=doc.querySelectorAll('section[id]');
     if(!secs.length) secs=doc.querySelectorAll('section');

← 9b4c3ad slideshow: /slideshow deck + Present button (parses the deli  ·  back to Fantasea Consulting  ·  chore: v1.2.0 (session close — LHF/SWOT/Slideshow) ec8946e →