Lighter email header, open tracking, and full funnel events on the site
This commit is contained in:
parent
776364ba74
commit
daf2d7460a
4 changed files with 88 additions and 17 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 +
|
||||||
|
|
|
||||||
|
|
@ -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.";
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue