PageSourceSearch

https://becomeawritertoday.com/conversion-events.js?v=20260908

js becomeawritertoday.com collected 2026-10-03 23:31:13 UTC 8,400 bytes, 238 lines download raw bytes

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.