CyberMax
Deploy utm-builder to the hub
71993f0 verified
Raw History Blame Contribute Delete
6.95 kB
/* 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);