/* * 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 }); } })();