← 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
M BRIEF.mdM public/index.htmlM server.js
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 →