← 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 →