[object Object]

← 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

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,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+  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 →