← back to Prestige Car Wash
Services: per-service 'Book this' deep-links pre-select the service on /contact
9d17487279e86dbdd64a3d679a14ad36fabc1a18 · 2026-07-26 20:41:42 -0700 · Steve Abrams
DTD A (3/5). Service cards link to /contact?service=<name>; contact form pre-selects
the matching real option (exact-match, never DOM-injected). Contrarian FIX-FIRST:
escape option name/price (quote/angle-bracket corruption), suppress service pre-select
under ?intent=membership (no contradictory form state). Canonical verified: ?service=
variants already canonicalize to clean /contact (no duplicate-URL risk).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M public/contact.htmlM public/index.htmlM public/services.html
Diff
commit 9d17487279e86dbdd64a3d679a14ad36fabc1a18
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jul 26 20:41:42 2026 -0700
Services: per-service 'Book this' deep-links pre-select the service on /contact
DTD A (3/5). Service cards link to /contact?service=<name>; contact form pre-selects
the matching real option (exact-match, never DOM-injected). Contrarian FIX-FIRST:
escape option name/price (quote/angle-bracket corruption), suppress service pre-select
under ?intent=membership (no contradictory form state). Canonical verified: ?service=
variants already canonicalize to clean /contact (no duplicate-URL risk).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
public/contact.html | 21 ++++++++++++++++++++-
public/index.html | 2 +-
public/services.html | 2 +-
3 files changed, 22 insertions(+), 3 deletions(-)
diff --git a/public/contact.html b/public/contact.html
index 87599cb..0ee7f51 100644
--- a/public/contact.html
+++ b/public/contact.html
@@ -66,8 +66,27 @@ fetch('/api/wait').then(r=>r.json()).then(w=>{
el.innerHTML=`⏱️ ${w.day} is typically <b>${w.level}</b> — ${w.phrase} <span style="opacity:.65">· book ahead to skip it</span>`;
el.title=w.basis||'';el.style.display='';
}).catch(()=>{});
+// Deep-link pre-select: /contact?service=<name> from a service card lands here with the
+// service already chosen (one fewer step). We only select if it matches a REAL service
+// name from the list — the param is never written into the DOM, so no injection risk.
+const _p=new URLSearchParams(location.search);
+// The membership flow owns the whole form (its own h2 + message), so don't ALSO cross-wire
+// a service pre-select into it — that would leave a contradictory "Join membership" heading
+// with a service chosen. Service deep-link only applies outside the membership flow.
+const qsService=_p.get('intent')==='membership'?null:_p.get('service');
fetch('/api/services?sort=featured').then(r=>r.json()).then(list=>{
- document.getElementById('svcSel').innerHTML += list.map(s=>`<option value="${s.name}">${s.name} — ${s.price_display||('$'+s.price)}</option>`).join('');
+ const sel=document.getElementById('svcSel');
+ // Escape option name/price so a service named with a quote/angle-bracket can't corrupt the
+ // option markup (which would silently break value-matching and POST the wrong service).
+ const e=s=>String(s==null?'':s).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>');
+ sel.innerHTML += list.map(s=>`<option value="${e(s.name)}">${e(s.name)} — ${e(s.price_display||('$'+s.price))}</option>`).join('');
+ if(qsService){
+ const match=list.find(s=>s.name===qsService); // exact match against the real list
+ if(match){
+ sel.value=match.name; // browser decodes the escaped attr, so this still matches
+ const h=document.querySelector('h2'); if(h) h.textContent=`Book: ${match.name}`;
+ }
+ }
});
document.getElementById('f').addEventListener('submit',async e=>{
e.preventDefault();
diff --git a/public/index.html b/public/index.html
index 2fb6467..69c2d0d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -126,7 +126,7 @@ fetch('/api/services?sort=featured').then(r=>r.json()).then(list=>{
<div class="cat">${s.category}</div>
<div class="ttl">${s.name}</div>
<div class="blurb">${s.blurb}</div>
- <div class="row"><span class="price">${s.price_display||('$'+s.price)}</span><a class="btn" href="/contact">Book</a></div>
+ <div class="row"><span class="price">${s.price_display||('$'+s.price)}</span><a class="btn" href="/contact?service=${encodeURIComponent(s.name)}">Book</a></div>
</div>
</div>`).join('');
});
diff --git a/public/services.html b/public/services.html
index aacd751..5c8ab4c 100644
--- a/public/services.html
+++ b/public/services.html
@@ -108,7 +108,7 @@ function card(s){
<div class="ttl">${s.name}</div>
<div class="blurb">${s.blurb}</div>
<div class="tags">${(s.highlights||[]).map(h=>`<span class="tag">${h}</span>`).join('')}</div>
- <div class="row" style="margin-top:10px"><span class="price">${s.price_display||('$'+s.price)}</span><a class="btn" href="/contact">Book</a></div>
+ <div class="row" style="margin-top:10px"><span class="price">${s.price_display||('$'+s.price)}</span><a class="btn" href="/contact?service=${encodeURIComponent(s.name)}">Book</a></div>
</div>
</div>`;
}
← 0760139 Mobile: persistent Book + click-to-call action bar on custom
·
back to Prestige Car Wash
·
Discovery: honest 'find us & review' trust block (real off-s d13f501 →