[object Object]

← back to Dw Domain Fleet

fix(catalog): sort dropdown was dead on all 8 funnel sites — CSP blocked its inline onchange (TK-11470)

b4b8158578671d0fee39d55399502ef3ad80700b · 2026-09-11 12:30:06 -0700 · Steve Abrams

Helmet is configured without useDefaults:false, so it emits script-src-attr 'none'
even though server.js's directives never mention it. That silently killed the sort
<select>'s inline onchange="...submit()" on every catalog page: picking a sort did
nothing, the URL stayed /catalog, and the grid kept its default order. Server-side
sorting was healthy the whole time — only the control was dead, so the feature was
reachable only by hand-editing the URL.

Replaces the inline handler with an addEventListener in the existing (CSP-allowed)
<script> block, and while there:
  - saved sort now actually re-orders via the server instead of only relabelling the
    dropdown, which previously showed e.g. "SKU A→Z" over a newest-ordered grid
  - a sort change resets to page 1 and preserves q= and any other query params
  - localStorage reads/writes are try/caught so denied storage can't break the page
  - saved density is range-clamped to the slider's 3..8
  - form submit/appendChild are called via prototype to survive DOM clobbering from
    a query param literally named submit or appendChild

Verified with a negative-tested cross-engine harness (Chromium + WebKit, real HTTPS
loopback because upgrade-insecure-requests breaks form GETs over plain http in
WebKit): 2/36 PASS against the broken baseline, 36/36 PASS after, plus 26/26 on a
second site. Real browsing shows 0 console errors.

Local adoption only — the Kamatera deploy stays gated (memo in pending-approval).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AyGLosyy49Y6TsyiwWQWnx

Files touched

Diff

commit b4b8158578671d0fee39d55399502ef3ad80700b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 11 12:30:06 2026 -0700

    fix(catalog): sort dropdown was dead on all 8 funnel sites — CSP blocked its inline onchange (TK-11470)
    
    Helmet is configured without useDefaults:false, so it emits script-src-attr 'none'
    even though server.js's directives never mention it. That silently killed the sort
    <select>'s inline onchange="...submit()" on every catalog page: picking a sort did
    nothing, the URL stayed /catalog, and the grid kept its default order. Server-side
    sorting was healthy the whole time — only the control was dead, so the feature was
    reachable only by hand-editing the URL.
    
    Replaces the inline handler with an addEventListener in the existing (CSP-allowed)
    <script> block, and while there:
      - saved sort now actually re-orders via the server instead of only relabelling the
        dropdown, which previously showed e.g. "SKU A→Z" over a newest-ordered grid
      - a sort change resets to page 1 and preserves q= and any other query params
      - localStorage reads/writes are try/caught so denied storage can't break the page
      - saved density is range-clamped to the slider's 3..8
      - form submit/appendChild are called via prototype to survive DOM clobbering from
        a query param literally named submit or appendChild
    
    Verified with a negative-tested cross-engine harness (Chromium + WebKit, real HTTPS
    loopback because upgrade-insecure-requests breaks form GETs over plain http in
    WebKit): 2/36 PASS against the broken baseline, 36/36 PASS after, plus 26/26 on a
    second site. Real browsing shows 0 console errors.
    
    Local adoption only — the Kamatera deploy stays gated (memo in pending-approval).
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AyGLosyy49Y6TsyiwWQWnx
---
 shared/render.js | 33 +++++++++++++++++++++------------
 1 file changed, 21 insertions(+), 12 deletions(-)

diff --git a/shared/render.js b/shared/render.js
index a7c1d28..b792192 100644
--- a/shared/render.js
+++ b/shared/render.js
@@ -745,7 +745,7 @@ function catalogPage(cfg, products, q, sort, page, pages, total) {
     <div class="ctl-sort">
       <input type="search" name="q" value="${esc(q)}" placeholder="Search ${total} patterns…">
       <label for="sortSel">Sort</label>
-      <select name="sort" id="sortSel" onchange="document.getElementById('ctlForm').submit()">
+      <select name="sort" id="sortSel">
         ${sopt('newest','Newest')}${sopt('color','Color')}${sopt('light-dark','Light → Dark')}
         ${sopt('dark-light','Dark → Light')}${sopt('style','Style')}${sopt('sku','SKU A→Z')}
         ${sopt('title','Title A→Z')}${sopt('price-asc','Price ↑')}${sopt('price-desc','Price ↓')}
@@ -767,20 +767,29 @@ ${script(cfg)}
 (function(){
   var dk='${cfg.slug}_density',sk='${cfg.slug}_sort';
   var d=document.getElementById('densSel'),g=document.getElementById('grid');
-  // hydrate density from localStorage BEFORE first paint (no flash from 5 → saved)
-  var sv=localStorage.getItem(dk);if(sv)d.value=sv;
+  var ss=document.getElementById('sortSel'),form=document.getElementById('ctlForm');
+  function read(k){try{return localStorage.getItem(k);}catch(e){return null;}}
+  function save(k,v){try{localStorage.setItem(k,v);}catch(e){}}
+  var sv=read(dk);if(sv && +sv>=3 && +sv<=8)d.value=sv;
   function apply(){g.style.setProperty('--cols',d.value);}
-  d.addEventListener('input',function(){apply();localStorage.setItem(dk,d.value);});
+  d.addEventListener('input',function(){apply();save(dk,d.value);});
   apply();
-  // hydrate sort dropdown from localStorage when URL has no ?sort= override
-  // (so the dropdown shows the user's last choice on a fresh /catalog visit)
-  var ss=document.getElementById('sortSel');
-  var ssSaved=localStorage.getItem(sk);
-  if(ssSaved && !/[?&]sort=/.test(location.search)){
-    var ok=Array.prototype.some.call(ss.options,function(o){return o.value===ssSaved;});
-    if(ok)ss.value=ssSaved;
+  // Successful GET controls preserve extra query values; a new selection starts page one.
+  var params=new URLSearchParams(location.search);
+  params.forEach(function(value,key){
+    if(key==='q'||key==='sort'||key==='page')return;
+    var input=document.createElement('input');input.type='hidden';
+    input.name=key;input.value=value;Node.prototype.appendChild.call(form,input);
+  });
+  ss.addEventListener('change',function(){save(sk,ss.value);HTMLFormElement.prototype.requestSubmit.call(form);});
+  var saved=read(sk);
+  var valid=Array.prototype.some.call(ss.options,function(o){return o.value===saved;});
+  if(!params.has('sort') && valid && saved!==ss.value){
+    // Request the saved ordering from the server; never merely relabel an unsorted grid.
+    params.set('sort',saved);params.delete('page');
+    location.replace(location.pathname+'?'+params.toString()+location.hash);
   }
-  ss.addEventListener('change',function(){localStorage.setItem(sk,ss.value);});
+
 })();
 </script>
 </body></html>`;

← 6a1fcbb deploy-fleet: RSYNC_EXTRA_EXCLUDES, so one agent's approved  ·  back to Dw Domain Fleet  ·  fix(catalog): guard requestSubmit — Safari <16 would have re 95a4e5a →