Spaces:
Running
Running
File size: 6,954 Bytes
71993f0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | /* UTM Builder core (pure, no DOM). © 2026 CyberMax. All rights reserved. */
(function (root) {
"use strict";
var KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_id", "utm_term", "utm_content"];
// Mediums that GA4's default channel groups recognise (patterns from Google's published rules, simplified).
var KNOWN_MEDIUM = /^(.*cp.*|ppc|retargeting|paid.*|e[-_ ]?mail|display|banner|expandable|interstitial|affiliate|referral|app|link|social|social[-_ ]network|social[-_ ]media|sm|organic|video|audio|sms|push|mobile|notification|qr|print|offline)$/;
var PRESETS = {
"Google Ads": { utm_source: "google", utm_medium: "cpc" },
"Microsoft Ads": { utm_source: "bing", utm_medium: "cpc" },
"Meta Ads": { utm_source: "facebook", utm_medium: "paid_social" },
"Instagram bio": { utm_source: "instagram", utm_medium: "social" },
"LinkedIn post": { utm_source: "linkedin", utm_medium: "social" },
"X / Twitter": { utm_source: "twitter", utm_medium: "social" },
"TikTok": { utm_source: "tiktok", utm_medium: "social" },
"YouTube": { utm_source: "youtube", utm_medium: "video" },
"Newsletter": { utm_source: "newsletter", utm_medium: "email" },
"QR / print": { utm_source: "qr", utm_medium: "print" },
"Affiliate": { utm_source: "partner", utm_medium: "affiliate" }
};
function clean(v, opts) {
v = String(v == null ? "" : v).trim();
if (opts.lower) v = v.toLowerCase();
if (opts.space && opts.space !== "keep") v = v.replace(/\s+/g, opts.space);
return v;
}
function normalizeBase(base) {
base = String(base || "").trim();
if (!base) return { error: "Enter the page URL you want to track." };
var added = false;
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(base)) { base = "https://" + base; added = true; }
var u;
try { u = new URL(base); } catch (e) { return { error: "That doesn't look like a valid URL." }; }
if (!/^https?:$/.test(u.protocol)) return { error: "Use an http:// or https:// URL." };
if (!/\./.test(u.hostname) && u.hostname !== "localhost") return { error: "That URL has no domain name." };
return { url: u, added: added };
}
// Build a tagged URL. params: {utm_source, ...}; opts: {lower: bool, space: "-"|"_"|"%20"|"keep"}.
function build(base, params, opts) {
opts = opts || { lower: true, space: "-" };
var warnings = [];
var nb = normalizeBase(base);
if (nb.error) return { url: "", error: nb.error, warnings: [] };
var u = nb.url;
if (nb.added) warnings.push("Added https:// to the URL.");
var replaced = [];
var kept = [];
u.search.replace(/^\?/, "").split("&").forEach(function (pair) {
if (!pair) return;
var k = decodeURIComponent(pair.split("=")[0].replace(/\+/g, " ")).toLowerCase();
if (KEYS.indexOf(k) >= 0) replaced.push(k); else kept.push(pair);
});
var tags = [];
KEYS.forEach(function (k) {
var raw = params[k];
var v = clean(raw, opts);
if (v) tags.push(k + "=" + encodeURIComponent(v).replace(/%2F/gi, "/"));
if (raw && /\s/.test(String(raw).trim()) && opts.space === "keep") warnings.push(k + " contains spaces; they'll be encoded as %20.");
if (raw && !opts.lower && /[A-Z]/.test(raw)) warnings.push(k + " has capitals. Analytics treats \"Facebook\" and \"facebook\" as different sources.");
});
["utm_source", "utm_medium", "utm_campaign"].forEach(function (k) {
if (!clean(params[k], opts)) warnings.push(k + " is empty. GA4 needs source, medium and campaign to report the visit properly.");
});
var med = clean(params.utm_medium, opts);
if (med && !KNOWN_MEDIUM.test(med.toLowerCase())) warnings.push("utm_medium \"" + med + "\" isn't one GA4's default channel groups recognise, so these visits may show as \"Unassigned\". Try cpc, email, social, paid_social, display, affiliate or referral.");
if (replaced.length) warnings.push("Replaced existing " + replaced.join(", ") + " in the URL.");
var search = kept.concat(tags).join("&");
var out = u.origin + u.pathname + (search ? "?" + search : "") + u.hash;
if (out.length > 2000) warnings.push("The URL is over 2,000 characters; some platforms truncate long links.");
return { url: out, warnings: warnings, tagged: tags.length > 0 };
}
function parse(url) {
var nb = normalizeBase(url);
if (nb.error) return { error: nb.error };
var u = nb.url, utm = {}, other = [];
u.searchParams.forEach(function (v, k) {
if (/^utm_/i.test(k)) utm[k.toLowerCase()] = v; else other.push(k + "=" + v);
});
var cleanUrl = u.origin + u.pathname;
var rest = [];
u.search.replace(/^\?/, "").split("&").forEach(function (p) { if (p && !/^utm_/i.test(p)) rest.push(p); });
if (rest.length) cleanUrl += "?" + rest.join("&");
cleanUrl += u.hash;
return { utm: utm, clean: cleanUrl, other: other };
}
// CSV line splitter with quotes.
function splitCsv(line) {
var out = [], cur = "", q = false;
for (var i = 0; i < line.length; i++) {
var c = line[i];
if (q) { if (c === '"' && line[i + 1] === '"') { cur += '"'; i++; } else if (c === '"') q = false; else cur += c; }
else if (c === '"') q = true;
else if (c === "," || c === "\t") { out.push(cur); cur = ""; }
else cur += c;
}
out.push(cur);
return out.map(function (s) { return s.trim(); });
}
// Bulk: each line "url" or "url,source,medium,campaign,term,content". Blank columns use defaults.
var BULK_COLS = ["url", "utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
function bulk(text, defaults, opts) {
var lines = String(text || "").split(/\r?\n/).map(function (l) { return l.trim(); }).filter(Boolean);
var cols = BULK_COLS;
if (lines.length && /^url\b/i.test(lines[0])) {
cols = splitCsv(lines.shift()).map(function (h) { h = h.toLowerCase().replace(/^utm_/, ""); return h === "url" ? "url" : "utm_" + h; });
}
return lines.slice(0, 5000).map(function (line) {
var cells = splitCsv(line), p = {};
KEYS.forEach(function (k) { p[k] = defaults[k] || ""; });
var url = "";
cols.forEach(function (c, i) { if (c === "url") url = cells[i] || ""; else if (cells[i]) p[c] = cells[i]; });
var r = build(url, p, opts);
return { input: url, url: r.url, error: r.error || "", warnings: r.warnings || [] };
});
}
function csvCell(s) { s = String(s == null ? "" : s); return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; }
function toCsv(rows) {
return "input_url,tagged_url,error\n" + rows.map(function (r) { return [r.input, r.url, r.error].map(csvCell).join(","); }).join("\n") + "\n";
}
var api = { KEYS: KEYS, PRESETS: PRESETS, build: build, parse: parse, bulk: bulk, toCsv: toCsv, splitCsv: splitCsv };
root.UtmCore = api;
if (typeof module !== "undefined") module.exports = api;
})(typeof window !== "undefined" ? window : globalThis);
|