Track outbound clicks as Plausible custom events
js/plausible-events.js classifies every outbound click: Affiliate Click — host in AFFILIATES map, or rel=sponsored Outbound Click — any other external link props: network, destination, destination_path, placement (utm_medium), link_text, lang. Internal links, in-page anchors, mailto: and tel: are ignored; data-plausible-skip opts an element out. Adding a new partner is one line in AFFILIATES. Loaded on 1206 pages before </body>. Create the two goals in Plausible (Settings -> Goals -> Custom event) named exactly 'Affiliate Click' and 'Outbound Click' for them to show as conversions.
This commit is contained in:
1 parent
3bd7001795
commit
21b274f91f
1207 files changed
+1349
-6
No files matched your search
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Outbound click tracking for Plausible.
|
||||
*
|
||||
* Sends two custom events so affiliate clicks can be separated from ordinary
|
||||
* outbound links — and so a new partner can be added by editing AFFILIATES only:
|
||||
*
|
||||
* Affiliate Click props: network, destination, destination_path, placement, link_text, lang
|
||||
* Outbound Click same props (network = "none")
|
||||
*
|
||||
* Goals must exist in Plausible (Settings -> Goals -> Custom event) with exactly
|
||||
* these names: "Affiliate Click" and "Outbound Click".
|
||||
*
|
||||
* Adding a partner: AFFILIATES["host.tld"] = {network: "short-name"}.
|
||||
* Links carrying rel="sponsored" are treated as affiliate even when unlisted.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// --- configuration -------------------------------------------------------
|
||||
var AFFILIATES = {
|
||||
"odoo4projects.com": { network: "odoo4projects" }
|
||||
// "example-partner.com": { network: "example" },
|
||||
};
|
||||
|
||||
var OWN_HOSTS = ["odoo-expertos.com"]; // apex + any subdomain of it
|
||||
|
||||
// Scroll-depth events are OFF by default: turn this on only if no other
|
||||
// scroll tracking runs on the site, otherwise every scroll is counted twice.
|
||||
var SCROLL_DEPTH = false;
|
||||
var SCROLL_THRESHOLDS = [25, 50, 75, 100];
|
||||
|
||||
// --- helpers -------------------------------------------------------------
|
||||
function trim(value, max) {
|
||||
if (value === null || value === undefined) return "";
|
||||
return String(value)
|
||||
.replace(/[\r\n\t]+/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.slice(0, max || 120);
|
||||
}
|
||||
|
||||
function hostOf(url) {
|
||||
try {
|
||||
return url.hostname.toLowerCase().replace(/^www\./, "");
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function isOwnHost(host) {
|
||||
for (var i = 0; i < OWN_HOSTS.length; i++) {
|
||||
var own = OWN_HOSTS[i];
|
||||
if (host === own || host.endsWith("." + own)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function paramName(url, name) {
|
||||
try {
|
||||
return url.searchParams.get(name) || "";
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function send(eventName, props) {
|
||||
if (typeof window.plausible !== "function") return;
|
||||
var clean = {};
|
||||
for (var k in props) {
|
||||
if (Object.prototype.hasOwnProperty.call(props, k) && props[k]) clean[k] = props[k];
|
||||
}
|
||||
window.plausible(eventName, { props: clean });
|
||||
}
|
||||
|
||||
// --- click handler -------------------------------------------------------
|
||||
function onClick(evt) {
|
||||
if (evt.type === "auxclick" && evt.button !== 1) return;
|
||||
|
||||
var node = evt.target;
|
||||
while (node && node.nodeName !== "A") node = node.parentNode;
|
||||
if (!node || !node.href) return;
|
||||
|
||||
// explicit opt-out
|
||||
if (node.hasAttribute("data-plausible-skip")) return;
|
||||
|
||||
var url;
|
||||
try {
|
||||
url = new URL(node.href);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") return; // mailto:, tel:, javascript:
|
||||
|
||||
var host = hostOf(url);
|
||||
if (!host || isOwnHost(host)) return; // internal navigation
|
||||
try {
|
||||
if (url.origin === window.location.origin) return; // same origin, incl. in-page anchors
|
||||
} catch (e) {}
|
||||
|
||||
var rel = (node.getAttribute("rel") || "").toLowerCase();
|
||||
var affiliate = AFFILIATES[host];
|
||||
var isAffiliate = !!affiliate || rel.indexOf("sponsored") !== -1;
|
||||
|
||||
var props = {
|
||||
network: affiliate ? affiliate.network : isAffiliate ? host : "none",
|
||||
destination: host,
|
||||
destination_path: url.pathname || "/",
|
||||
placement: paramName(url, "utm_medium") || "unknown",
|
||||
link_text: trim(node.textContent, 60),
|
||||
lang: trim((document.documentElement.lang || "").slice(0, 5), 5)
|
||||
};
|
||||
|
||||
send(isAffiliate ? "Affiliate Click" : "Outbound Click", props);
|
||||
}
|
||||
|
||||
document.addEventListener("click", onClick, true);
|
||||
document.addEventListener("auxclick", onClick, true);
|
||||
|
||||
// --- optional scroll depth ----------------------------------------------
|
||||
if (SCROLL_DEPTH) {
|
||||
var fired = {};
|
||||
var onScroll = function () {
|
||||
var doc = document.documentElement;
|
||||
var total = doc.scrollHeight - window.innerHeight;
|
||||
if (total <= 0) return;
|
||||
var pct = Math.round((window.scrollY / total) * 100);
|
||||
for (var i = 0; i < SCROLL_THRESHOLDS.length; i++) {
|
||||
var step = SCROLL_THRESHOLDS[i];
|
||||
if (pct >= step && !fired[step]) {
|
||||
fired[step] = true;
|
||||
send("Scroll Depth", { depth: String(step) });
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
}
|
||||
})();
|
||||
Reference in new issue
Block a user