[object Object]

← back to Bounce Studio

funnel: first-class reference-link field on #start (wired end-to-end, ref hardened) + BRIEF funnel-wiring notes

0689fe1e6da325b9a93cf3dbf21dc0fa01290576 · 2026-08-11 12:41:35 -0700 · steve@designerwallcoverings.com

Files touched

Diff

commit 0689fe1e6da325b9a93cf3dbf21dc0fa01290576
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Tue Aug 11 12:41:35 2026 -0700

    funnel: first-class reference-link field on #start (wired end-to-end, ref hardened) + BRIEF funnel-wiring notes
---
 BRIEF.md          | 21 +++++++++++++++++++--
 public/index.html |  7 +++++--
 server.js         | 12 +++++++++---
 3 files changed, 33 insertions(+), 7 deletions(-)

diff --git a/BRIEF.md b/BRIEF.md
index 163991e..090a694 100644
--- a/BRIEF.md
+++ b/BRIEF.md
@@ -34,6 +34,23 @@
 
 ## Status
 - [x] Intake + brief (this file)
-- [ ] Design direction + build landing
-- [ ] Local viewer + /5x verify
+- [x] Design direction + build landing
+- [x] Local viewer + /5x verify
+- [x] Deployed live on Kamatera :9955 → sixskills.agentabrams.com
 - [ ] Stop for Steve's go before any deploy/DNS/publish
+
+## Funnel wiring (TK-10471, 2026-08-11)
+The offer is now **executable**, not just described:
+- **First-class reference link** on `#start` — the pitch is "send a link", so the
+  ref URL is its own field (was buried in the optional note). Wired end-to-end:
+  form input + JS validation → `server.js` stores a hardened `ref` in
+  `data/waitlist.jsonl` (bare-domain normalized, junk like `javascript:` dropped).
+- **`bounce-intake` agent** (`~/.claude/agents/bounce-intake.md`) triages each new
+  waitlist lead via `claude-webdev-accelerator/scripts/bounce-intake.js` →
+  scaffolds `clients/<slug>/BRIEF.md` + drafts a **gated** reply to
+  pending-approval (no email is ever sent).
+- **`bounce-build` agent** (`~/.claude/agents/bounce-build.md`) runs the real
+  `ACCELERATOR.md` pipeline for a scaffolded engagement, mapping the six marketing
+  skill names (brief/front-end/design/components/motion/media) to real skills.
+- **Deploy of the landing change is GATED** — memo in
+  `~/.claude/yolo-queue/pending-approval/`.
diff --git a/public/index.html b/public/index.html
index 567f69d..0215107 100644
--- a/public/index.html
+++ b/public/index.html
@@ -299,6 +299,7 @@
         <input type="email" id="email" name="email" placeholder="you@company.com" autocomplete="email" required>
         <button class="btn" type="submit">request a build →</button>
       </div>
+      <input type="url" id="ref" name="ref" inputmode="url" autocomplete="url" placeholder="the link — a site or look you love (optional)">
       <select id="budget" name="budget" aria-label="what are you building">
         <option value="">what are you building? (optional)</option>
         <option>a new marketing site / landing</option>
@@ -306,7 +307,7 @@
         <option>a product / app front-end</option>
         <option>something else</option>
       </select>
-      <textarea id="note" name="note" rows="2" placeholder="drop a reference link or a sentence (optional)"></textarea>
+      <textarea id="note" name="note" rows="2" placeholder="anything else worth knowing? (optional)"></textarea>
       <div class="msg" id="msg" role="status" aria-live="polite"></div>
       <div class="fineprint">no spam, no list. this goes straight to the build queue.</div>
     </form>
@@ -332,12 +333,14 @@
     ev.preventDefault();
     const email = document.getElementById('email').value.trim();
     const budget = document.getElementById('budget').value;
+    const ref = document.getElementById('ref').value.trim();
     const note = document.getElementById('note').value.trim();
     msg.className='msg'; msg.textContent='';
     if(!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)){ msg.className='msg err'; msg.textContent='that email looks off — check it?'; return; }
+    if(ref && !/^(https?:\/\/)?[\w-]+(\.[\w-]{2,})+/.test(ref)){ msg.className='msg err'; msg.textContent='that link looks off — paste a full url?'; return; }
     msg.textContent='sending…';
     try{
-      const r = await fetch('/api/waitlist',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email,budget,note})});
+      const r = await fetch('/api/waitlist',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email,budget,ref,note})});
       const j = await r.json();
       if(j.ok){ msg.className='msg ok'; msg.textContent="you're in the queue. we'll be in touch with a scoped brief."; form.reset(); }
       else { msg.className='msg err'; msg.textContent = j.error || 'something went wrong — try again.'; }
diff --git a/server.js b/server.js
index 1cc2414..8e6ba39 100644
--- a/server.js
+++ b/server.js
@@ -59,11 +59,17 @@ const server = http.createServer((req, res) => {
     let body = '';
     req.on('data', (c) => { body += c; if (body.length > 1e4) req.destroy(); });
     req.on('end', () => {
-      let email = '', note = '', budget = '';
-      try { const j = JSON.parse(body || '{}'); email = (j.email || '').trim(); note = (j.note || '').toString().slice(0, 500); budget = (j.budget || '').toString().slice(0, 40); } catch (_) {}
+      let email = '', note = '', budget = '', ref = '';
+      try { const j = JSON.parse(body || '{}'); email = (j.email || '').trim(); note = (j.note || '').toString().slice(0, 500); budget = (j.budget || '').toString().slice(0, 40); ref = (j.ref || '').toString().trim().slice(0, 300); } catch (_) {}
       const ok = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email);
       if (!ok) { res.writeHead(400, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ ok: false, error: 'invalid email' })); }
-      const rec = { ts: new Date().toISOString(), email, budget, note, ua: (req.headers['user-agent'] || '').slice(0, 200) };
+      // normalize a bare-domain reference to a URL so downstream (bounce-intake)
+      // always sees a link, then HARD-VALIDATE: only keep a real dot-domain
+      // http(s) url. Drops junk like "javascript:alert(1)" (which naive prepend
+      // would turn into "https://javascript:alert(1)") — store '' instead.
+      if (ref && !/^https?:\/\//i.test(ref)) ref = 'https://' + ref;
+      if (ref && !/^https?:\/\/[^\s/.]+\.[^\s]{2,}$/i.test(ref)) ref = '';
+      const rec = { ts: new Date().toISOString(), email, budget, ref, note, ua: (req.headers['user-agent'] || '').slice(0, 200) };
       fs.appendFile(path.join(DATA, 'waitlist.jsonl'), JSON.stringify(rec) + '\n', (e) => {
         if (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ ok: false, error: 'write failed' })); }
         res.writeHead(200, { 'Content-Type': 'application/json' });

← e4ae877 Revert "AdSense: loader + ads.txt (ca-pub-5278231299883833)  ·  back to Bounce Studio  ·  chore: session-close hardening — 413 on oversize body, path. 36dd476 →