Lighter email header, open tracking, and full funnel events on the site

This commit is contained in:
Devin Willis 2026-08-26 15:22:27 -04:00
parent 776364ba74
commit daf2d7460a
No known key found for this signature in database
GPG key ID: 3776B3C5AFF3FFEA
4 changed files with 88 additions and 17 deletions

View file

@ -64,7 +64,20 @@ function esc(s) {
.replace(/"/g, """).replace(/'/g, "'"); .replace(/"/g, """).replace(/'/g, "'");
} }
var HERO = "https://nyc3.digitaloceanspaces.com/corben-sites/images/c23ea202-5442-41b4-9a1b-1586e81c50eb/e7119448-83ba-4da4-95d8-b64045c21160/email-header-trim.png"; var HERO = "https://nyc3.digitaloceanspaces.com/corben-sites/images/c23ea202-5442-41b4-9a1b-1586e81c50eb/5b98d309-7283-4202-bf43-263dbcec74c2/email-header-web.png";
var SITE_ID = "4ae63819-3f63-4fea-b0de-eb1e5befe607";
var PIXEL = "https://api.corben.world/analytics/open.gif";
// 1x1 open pixel. Opens skew high because Apple Mail Privacy Protection and
// Gmail's proxy prefetch images, so read it as a ceiling, not a headcount.
function openPixel(kind, who, when) {
return '<img src="' + PIXEL + '?s=' + SITE_ID +
'&k=' + encodeURIComponent(kind) +
'&e=' + who +
'&d=' + encodeURIComponent(when || '') +
'" width="1" height="1" alt="" style="display:block;width:1px;height:1px;border:0;opacity:0;">';
}
var UNSUB = "https://api.corben.world/campaigns/unsubscribe?user_id=c23ea202-5442-41b4-9a1b-1586e81c50eb&email="; var UNSUB = "https://api.corben.world/campaigns/unsubscribe?user_id=c23ea202-5442-41b4-9a1b-1586e81c50eb&email=";
var SANS = "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif"; var SANS = "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif";
@ -99,7 +112,7 @@ function shell(opts) {
'<a href="' + UNSUB + (opts.unsubscribeFor || '') + '" style="color:#8b89a6;text-decoration:underline;">Unsubscribe from these emails</a>' + '<a href="' + UNSUB + (opts.unsubscribeFor || '') + '" style="color:#8b89a6;text-decoration:underline;">Unsubscribe from these emails</a>' +
' and we will not email you about the Break again.' + ' and we will not email you about the Break again.' +
'</p></td></tr>' + '</p></td></tr>' +
'</table></td></tr></table></body></html>'; '</table></td></tr></table>' + (opts.pixel || '') + '</body></html>';
} }
// The date and place, sized like an invitation rather than a spec sheet. // The date and place, sized like an invitation rather than a spec sheet.
@ -178,6 +191,7 @@ var campaign = await tool("create_campaign", {
imageAlt: "A blueprint drawing of a pool table and the break shot.", imageAlt: "A blueprint drawing of a pool table and the break shot.",
body: body, body: body,
unsubscribeFor: '{{email}}', unsubscribeFor: '{{email}}',
pixel: openPixel('reminder', '{{email}}', target),
}), }),
text_body: "The Break is tomorrow, " + when + ".\n\n" + VENUE + ", " + STREET + ", " + CITY + text_body: "The Break is tomorrow, " + when + ".\n\n" + VENUE + ", " + STREET + ", " + CITY +
"\n7:00 to 9:00 PM. Free entry, 18 and over.\n\nBring a valid ID.\n\n" + SITE, "\n7:00 to 9:00 PM. Free entry, 18 and over.\n\nBring a valid ID.\n\n" + SITE,

View file

@ -43,7 +43,20 @@ function prettyDate(iso) {
}).format(d); }).format(d);
} }
var HERO = "https://nyc3.digitaloceanspaces.com/corben-sites/images/c23ea202-5442-41b4-9a1b-1586e81c50eb/e7119448-83ba-4da4-95d8-b64045c21160/email-header-trim.png"; var HERO = "https://nyc3.digitaloceanspaces.com/corben-sites/images/c23ea202-5442-41b4-9a1b-1586e81c50eb/5b98d309-7283-4202-bf43-263dbcec74c2/email-header-web.png";
var SITE_ID = "4ae63819-3f63-4fea-b0de-eb1e5befe607";
var PIXEL = "https://api.corben.world/analytics/open.gif";
// 1x1 open pixel. Opens skew high because Apple Mail Privacy Protection and
// Gmail's proxy prefetch images, so read it as a ceiling, not a headcount.
function openPixel(kind, who, when) {
return '<img src="' + PIXEL + '?s=' + SITE_ID +
'&k=' + encodeURIComponent(kind) +
'&e=' + who +
'&d=' + encodeURIComponent(when || '') +
'" width="1" height="1" alt="" style="display:block;width:1px;height:1px;border:0;opacity:0;">';
}
var UNSUB = "https://api.corben.world/campaigns/unsubscribe?user_id=c23ea202-5442-41b4-9a1b-1586e81c50eb&email="; var UNSUB = "https://api.corben.world/campaigns/unsubscribe?user_id=c23ea202-5442-41b4-9a1b-1586e81c50eb&email=";
var SANS = "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif"; var SANS = "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif";
@ -78,7 +91,7 @@ function shell(opts) {
'<a href="' + UNSUB + (opts.unsubscribeFor || '') + '" style="color:#8b89a6;text-decoration:underline;">Unsubscribe from these emails</a>' + '<a href="' + UNSUB + (opts.unsubscribeFor || '') + '" style="color:#8b89a6;text-decoration:underline;">Unsubscribe from these emails</a>' +
' and we will not email you about the Break again.' + ' and we will not email you about the Break again.' +
'</p></td></tr>' + '</p></td></tr>' +
'</table></td></tr></table></body></html>'; '</table></td></tr></table>' + (opts.pixel || '') + '</body></html>';
} }
// The date and place, sized like an invitation rather than a spec sheet. // The date and place, sized like an invitation rather than a spec sheet.
@ -225,6 +238,7 @@ if (attending === "no") {
imageAlt: "A blueprint drawing of a pool table and the break shot.", imageAlt: "A blueprint drawing of a pool table and the break shot.",
body: backBody, body: backBody,
unsubscribeFor: encodeURIComponent(email), unsubscribeFor: encodeURIComponent(email),
pixel: openPixel('confirm-returning', encodeURIComponent(email), eventDate),
}), }),
text: "You are confirmed for this week, " + first + ".\n\n" + when + ", 7:00 to 9:00 PM\n" + text: "You are confirmed for this week, " + first + ".\n\n" + when + ", 7:00 to 9:00 PM\n" +
VENUE + "\n" + STREET + ", " + CITY + "\nFree entry. 18 and over, so bring an ID.\n\n" + SITE, VENUE + "\n" + STREET + ", " + CITY + "\nFree entry. 18 and over, so bring an ID.\n\n" + SITE,
@ -249,6 +263,7 @@ if (attending === "no") {
imageAlt: "A blueprint drawing of a pool table and the break shot.", imageAlt: "A blueprint drawing of a pool table and the break shot.",
body: confirmBody, body: confirmBody,
unsubscribeFor: encodeURIComponent(email), unsubscribeFor: encodeURIComponent(email),
pixel: openPixel('confirm-first', encodeURIComponent(email), eventDate),
}), }),
text: "You are on the list, " + first + ". There is nothing else to do before then.\n\n" + text: "You are on the list, " + first + ". There is nothing else to do before then.\n\n" +
when + ", 7:00 to 9:00 PM\n" + VENUE + "\n" + STREET + ", " + CITY + when + ", 7:00 to 9:00 PM\n" + VENUE + "\n" + STREET + ", " + CITY +

View file

@ -148,6 +148,44 @@
window.addEventListener("pagehide", function () { clearInterval(timer); }); window.addEventListener("pagehide", function () { clearInterval(timer); });
} }
/* ── Funnel milestones ───────────────────────────────────
The Corben tracker injected into every site already records pageviews,
clicks on [data-track], form_start, form_submit, scroll depth and
engagement time. These are the steps it cannot see on its own: reaching
the RSVP block, and whether the submission actually succeeded. Same
public /analytics/events endpoint the tracker posts to. */
var SITE_ID = (document.currentScript && document.currentScript.dataset.site) ||
(document.querySelector('script[data-site]') || {}).dataset;
if (SITE_ID && typeof SITE_ID === "object") SITE_ID = SITE_ID.site;
function track(type, data) {
if (!SITE_ID) return;
var body = JSON.stringify({
site_id: SITE_ID,
events: [{ event_type: type, page_path: location.pathname, event_data: data || {}, ts: Date.now() }],
});
// keepalive so an event fired as the tab closes still lands.
try {
fetch("https://api.corben.world/analytics/events", {
method: "POST", headers: { "Content-Type": "application/json" },
body: body, keepalive: true, mode: "cors",
}).catch(function () {});
} catch (e) {}
}
// Did they ever actually reach the RSVP block? Fires once.
var rsvpSection = document.getElementById("rsvp");
if (rsvpSection) {
var seen = new IntersectionObserver(function (entries, obs) {
if (!entries[0].isIntersecting) return;
track("rsvp_reached", { source: new URLSearchParams(location.search).get("utm_source") || "direct" });
obs.disconnect();
}, { threshold: 0.3 });
seen.observe(rsvpSection);
}
/* ── RSVP ── */ /* ── RSVP ── */
var form = document.getElementById("rsvp-form"); var form = document.getElementById("rsvp-form");
if (!form) return; if (!form) return;
@ -216,9 +254,11 @@
}) })
.then(function (body) { .then(function (body) {
if (body && body.ok === false) throw new Error(body.error || "rejected"); if (body && body.ok === false) throw new Error(body.error || "rejected");
track("rsvp_completed", { via: "form" });
showDone(); showDone();
}) })
.catch(function () { .catch(function (err) {
track("rsvp_failed", { via: "form", reason: String(err && err.message || "unknown").slice(0, 80) });
submit.removeAttribute("aria-busy"); submit.removeAttribute("aria-busy");
status.textContent = status.textContent =
"That did not go through. Try again, or email contact@onetouch.studio and we will add you by hand."; "That did not go through. Try again, or email contact@onetouch.studio and we will add you by hand.";
@ -279,9 +319,11 @@
.then(function (res) { if (!res.ok) throw new Error("HTTP " + res.status); return res.json().catch(function () { return {}; }); }) .then(function (res) { if (!res.ok) throw new Error("HTTP " + res.status); return res.json().catch(function () { return {}; }); })
.then(function (body) { .then(function (body) {
if (body && body.ok === false) throw new Error("rejected"); if (body && body.ok === false) throw new Error("rejected");
track("rsvp_completed", { via: "reminder-email", answer: answer });
showDone(answer); showDone(answer);
}) })
.catch(function () { .catch(function (err) {
track("rsvp_failed", { via: "reminder-email", answer: answer, reason: String(err && err.message || "unknown").slice(0, 80) });
submit.removeAttribute("aria-busy"); submit.removeAttribute("aria-busy");
status.textContent = "That link did not go through. Hit RSVP below and you are set."; status.textContent = "That link did not go through. Hit RSVP below and you are set.";
}); });

View file

@ -158,7 +158,7 @@
<a class="nav-link" href="#details">Details</a> <a class="nav-link" href="#details">Details</a>
<a class="nav-link" href="#expect">What to expect</a> <a class="nav-link" href="#expect">What to expect</a>
<a class="nav-link" href="#venue">The venue</a> <a class="nav-link" href="#venue">The venue</a>
<a class="nav-cta" href="#rsvp">RSVP</a> <a class="nav-cta" href="#rsvp" data-track="rsvp-nav">RSVP</a>
</nav> </nav>
</header> </header>
@ -171,8 +171,8 @@
<h1><%= site.name %></h1> <h1><%= site.name %></h1>
<p class="hero-sub"><%= heroLine %></p> <p class="hero-sub"><%= heroLine %></p>
<div class="hero-actions"> <div class="hero-actions">
<a class="btn btn-primary" href="#rsvp">RSVP</a> <a class="btn btn-primary" href="#rsvp" data-track="rsvp-hero">RSVP</a>
<a class="btn btn-ghost" href="#expect">What to expect</a> <a class="btn btn-ghost" href="#expect" data-track="hero-what-to-expect">What to expect</a>
</div> </div>
</div> </div>
<figure class="hero-figure"> <figure class="hero-figure">
@ -215,13 +215,13 @@
<% details.forEach(function (d) { %> <% details.forEach(function (d) { %>
<div class="detail"> <div class="detail">
<dt><%= d.label %></dt> <dt><%= d.label %></dt>
<dd><% if (d.href) { %><a href="<%= d.href %>" target="_blank" rel="noopener"><%= d.value %></a><% } else { %><%= d.value %><% } %></dd> <dd><% if (d.href) { %><a href="<%= d.href %>" target="_blank" rel="noopener" data-track="details-map"><%= d.value %></a><% } else { %><%= d.value %><% } %></dd>
</div> </div>
<% }) %> <% }) %>
</dl> </dl>
<p class="detail-foot"> <p class="detail-foot">
<span<% if (isTonight) { %> class="is-live"<% } %>><%= isTonight ? "Happening tonight" : `Next: ${nextShort}` %></span> <span<% if (isTonight) { %> class="is-live"<% } %>><%= isTonight ? "Happening tonight" : `Next: ${nextShort}` %></span>
<a class="ics" href="/assets/founders-break.ics">Add the whole series to your calendar</a> <a class="ics" href="/assets/founders-break.ics" data-track="add-to-calendar">Add the whole series to your calendar</a>
</p> </p>
</section> </section>
@ -259,10 +259,10 @@
<h2>We play at <%= V.name %>.</h2> <h2>We play at <%= V.name %>.</h2>
<p><%= V.note %></p> <p><%= V.note %></p>
<address> <address>
<a href="<%= V.mapUrl %>" target="_blank" rel="noopener"><%= V.street %><br><%= V.city %>, <%= V.state %> <%= V.zip %></a> <a href="<%= V.mapUrl %>" target="_blank" rel="noopener" data-track="venue-map"><%= V.street %><br><%= V.city %>, <%= V.state %> <%= V.zip %></a>
</address> </address>
<p class="venue-meta"> <p class="venue-meta">
<a href="<%= V.instagramUrl %>" target="_blank" rel="noopener">@<%= V.instagram %></a> <a href="<%= V.instagramUrl %>" target="_blank" rel="noopener" data-track="venue-instagram">@<%= V.instagram %></a>
<span>Pouring since <%= V.established %></span> <span>Pouring since <%= V.established %></span>
</p> </p>
</div> </div>
@ -307,7 +307,7 @@
<p class="field-help">A few words is plenty. It helps us introduce you to the right people.</p> <p class="field-help">A few words is plenty. It helps us introduce you to the right people.</p>
</div> </div>
<button class="btn btn-primary btn-block" type="submit" id="rsvp-submit">RSVP</button> <button class="btn btn-primary btn-block" type="submit" id="rsvp-submit" data-track="rsvp-submit">RSVP</button>
<p class="form-status" id="form-status" role="status" aria-live="polite"></p> <p class="form-status" id="form-status" role="status" aria-live="polite"></p>
<p class="fine">One email a week at most. Unsubscribe any time.</p> <p class="fine">One email a week at most. Unsubscribe any time.</p>
</form> </form>
@ -329,7 +329,7 @@
<div class="rsvp-done" id="rsvp-done" hidden> <div class="rsvp-done" id="rsvp-done" hidden>
<h3>You are on the list.</h3> <h3>You are on the list.</h3>
<p><%= isTonight ? "Tonight" : nextLong %> at <%= V.name %>, <%= V.street %>, <%= site.startTime %>. A confirmation is on its way to your inbox.</p> <p><%= isTonight ? "Tonight" : nextLong %> at <%= V.name %>, <%= V.street %>, <%= site.startTime %>. A confirmation is on its way to your inbox.</p>
<a class="ics" href="/assets/founders-break.ics">Add the whole series to your calendar</a> <a class="ics" href="/assets/founders-break.ics" data-track="add-to-calendar">Add the whole series to your calendar</a>
</div> </div>
</div> </div>
</section> </section>
@ -344,10 +344,10 @@
<span>Hosted by <%= site.host.name %></span> <span>Hosted by <%= site.host.name %></span>
</a> </a>
<a href="mailto:<%= site.host.email %>"><%= site.host.email %></a> <a href="mailto:<%= site.host.email %>"><%= site.host.email %></a>
<a href="<%= V.mapUrl %>" target="_blank" rel="noopener"><%= V.name %>, <%= V.street %></a> <a href="<%= V.mapUrl %>" target="_blank" rel="noopener" data-track="venue-map"><%= V.name %>, <%= V.street %></a>
</div> </div>
</footer> </footer>
<script src="/js/main.js" defer></script> <script src="/js/main.js" data-site="<%= tenant.id %>" defer></script>
</body> </body>
</html> </html>