1/* 2 * Conversion events. One vocabulary across every site in the portfolio, read 3 * from one shared PostHog project. This file is byte-identical in each site 4 * repo; per-site behaviour comes from window.__CONV, set in the layout before 5 * this script loads. Change every copy or none. 6 * 7 * Events this script can emit: 8 * Affiliate Click a click on an affiliate link (host list, rel=sponsored, 9 * data-evt="affiliate_click" or data-affiliate) 10 * Checkout Click a click on a Stripe payment link 11 * Email Signup a submit on any form with an email field that is not the 12 * contact form (fires on submit, before the ESP accepts it: 13 * the ESP count stays the authoritative signup number) 14 * Contact Submit a submit on the contact form 15 * Tool Started the first input/change/click inside a window.__CONV.tools 16 * container, at most once per page view (mount) 17 * Tool Completed the container's DOM mutates after Tool Started has fired 18 * (a rendered result), at most once per page view, and 19 * never before Tool Started unless that tool sets start:false 20 * <site-defined> window.__CONV.clicks / window.__CONV.forms map a selector 21 * to an event name; event null means ignore that form 22 * 23 * window.__CONV = { 24 * site: 'bawt', 25 * affiliateHosts: ['amazon.', 'go.becomeawritertoday.com'], // substring of hostname 26 * clicks: [{ sel: 'a[href^="/sponsor"]', event: 'Sponsor CTA Click' }], 27 * forms: [{ sel: '#buy-form', event: 'Checkout Start' }, { sel: '.ignore-me', event: null }], 28 * tools: [{ tool: 'syllable-counter', sel: '[data-tool="syllable-counter"]' }] 29 * // start: false on an entry suppresses Tool Started for that tool 30 * // (page-shaped sections, e.g. Leinster's calculators, where a 31 * // start event would just duplicate the pageview) and arms 32 * // Tool Completed to fire on the first container mutation alone. 33 * } 34 * 35 * Listeners bind on document in the capture phase, so they survive client-side 36 * navigation (Astro ClientRouter swaps) and run before a component's own 37 * handler calls preventDefault or stopPropagation. Captures use sendBeacon so 38 * a native form POST or an outbound navigation cannot lose the event. Tool 39 * tracking is (re)armed on DOMContentLoaded and on astro:page-load, since a 40 * client-side nav swaps in a new tool container without reloading this script. 41 */ 42(function () { 43 var cfg = window.__CONV || {}; 44 var CHECKOUT_HOSTS = ['buy.stripe.com', 'checkout.stripe.com']; 45 46 function capture(name, props) { 47 try { 48 var ph = window.posthog; 49 if (!ph || typeof ph.capture !== 'function') return; 50 ph.capture(name, props, { transport: 'sendBeacon' }); 51 } catch (e) { 52 /* analytics must never break the page */ 53 } 54 } 55 56 function base(extra) { 57 var out = { site: cfg.site || location.hostname, page: location.pathname }; 58 for (var k in extra) if (Object.prototype.hasOwnProperty.call(extra, k)) out[k] = extra[k]; 59 return out; 60 } 61 62 function hostOf(a) { 63 try { 64 return new URL(a.getAttribute('href'), location.href).hostname.toLowerCase(); 65 } catch (e) { 66 return ''; 67 } 68 } 69 70 function matches(el, sel) { 71 try { 72 return el.matches(sel); 73 } catch (e) { 74 return false; 75 } 76 } 77 78 function isAffiliate(a, host) { 79 if (matches(a, '[data-evt="affiliate_click"], [data-affiliate], [rel~="sponsored"]')) return true; 80 var hosts = cfg.affiliateHosts || []; 81 for (var i = 0; i < hosts.length; i++) { 82 if (host.indexOf(hosts[i]) !== -1) return true; 83 } 84 return false; 85 } 86 87 function formName(f) { 88 return ( 89 f.getAttribute('name') || 90 f.getAttribute('data-tag') || 91 f.getAttribute('data-upgrade-slug') || 92 f.getAttribute('data-source') || 93 f.id || 94 (f.getAttribute('action') || '').split('/').pop() || 95 'unnamed' 96 ); 97 } 98 99 function hiddenValue(f, names) { 100 for (var i = 0; i < names.length; i++) { 101 var el = f.querySelector('input[name="' + names[i] + '"]'); 102 if (el && el.value) return el.value; 103 } 104 return ''; 105 } 106 107 document.addEventListener( 108 'click', 109 function (ev) { 110 var t = ev.target; 111 if (!t || typeof t.closest !== 'function') return; 112 var a = t.closest('a[href]'); 113 if (!a) return; 114 var host = hostOf(a); 115 var text = (a.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 80); 116 var clicks = cfg.clicks || []; 117 for (var i = 0; i < clicks.length; i++) { 118 if (matches(a, clicks[i].sel)) { 119 if (clicks[i].event) capture(clicks[i].event, base({ url: a.href, text: text })); 120 return; 121 } 122 } 123 if (CHECKOUT_HOSTS.indexOf(host) !== -1) { 124 capture('Checkout Click', base({ url: a.href, text: text })); 125 return; 126 } 127 if (isAffiliate(a, host)) { 128 capture( 129 'Affiliate Click', 130 base({ 131 url: a.href, 132 host: host, 133 text: text, 134 context: a.getAttribute('data-context') || a.getAttribute('data-tag') || '', 135 }) 136 ); 137 } 138 }, 139 true 140 ); 141 142 document.addEventListener( 143 'submit', 144 function (ev) { 145 var f = ev.target; 146 if (!f || f.tagName !== 'FORM') return; 147 var forms = cfg.forms || []; 148 for (var i = 0; i < forms.length; i++) { 149 if (matches(f, forms[i].sel)) { 150 if (forms[i].event) capture(forms[i].event, base({ form: formName(f) })); 151 return; 152 } 153 } 154 var email = f.querySelector('input[type="email"]'); 155 if (!email) return;
156 if (matches(f, 'form[name="contact"], form[name*="contact"]')) { 157 capture('Contact Submit', base({ form: formName(f) })); 158 return; 159 } 160 if (!email.value || email.value.indexOf('@') === -1) return; 161 capture( 162 'Email Signup', 163 base({ 164 form: formName(f), 165 source: 166 f.getAttribute('data-source') || 167 f.getAttribute('data-tag') || 168 hiddenValue(f, ['source', 'follows', 'tag', 'signup_source']), 169 }) 170 ); 171 }, 172 true 173 ); 174 175 // Tool Started / Tool Completed. Config-driven per window.__CONV.tools; no 176 // per-tool branching lives here. toolState is rebuilt on every (re)init so a 177 // client-side nav to a different tool page starts a fresh started/completed 178 // pair rather than inheriting the previous page's. 179 var toolState = {}; 180 var toolObservers = []; 181 182 function initTools() { 183 for (var i = 0; i < toolObservers.length; i++) toolObservers[i].disconnect(); 184 toolObservers = []; 185 toolState = {}; 186 187 var tools = cfg.tools || []; 188 if (!tools.length || typeof MutationObserver === 'undefined') return; 189 190 tools.forEach(function (t) { 191 if (!t || !t.tool || !t.sel) return; 192 var el; 193 try { 194 el = document.querySelector(t.sel); 195 } catch (e) { 196 return; 197 } 198 if (!el) return; 199 200 var noStart = t.start === false; 201 toolState[t.tool] = { sel: t.sel, started: noStart, completed: false, noStart: noStart }; 202 203 var mo = new MutationObserver(function () { 204 var st = toolState[t.tool]; 205 if (!st || !st.started || st.completed) return; 206 st.completed = true; 207 capture('Tool Completed', base({ tool: t.tool })); 208 }); 209 mo.observe(el, { childList: true, subtree: true, characterData: true }); 210 toolObservers.push(mo); 211 }); 212 } 213 214 function handleToolInteraction(ev) { 215 var t = ev.target; 216 if (!t || typeof t.closest !== 'function') return; 217 for (var name in toolState) { 218 if (!Object.prototype.hasOwnProperty.call(toolState, name)) continue; 219 var st = toolState[name]; 220 if (st.started || st.noStart) continue; 221 if (t.closest(st.sel)) { 222 st.started = true; 223 capture('Tool Started', base({ tool: name })); 224 } 225 } 226 } 227 228 document.addEventListener('input', handleToolInteraction, true); 229 document.addEventListener('change', handleToolInteraction, true); 230 document.addEventListener('click', handleToolInteraction, true); 231 232 if (document.readyState === 'loading') { 233 document.addEventListener('DOMContentLoaded', initTools); 234 } else { 235 initTools(); 236 } 237 document.addEventListener('astro:page-load', initTools); 238})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.