Skip to content

Instantly share code, notes, and snippets.

@joshuafredrickson
Created October 1, 2025 21:28
Show Gist options
  • Select an option

  • Save joshuafredrickson/285a3b3da8218529e296e7052cc49a11 to your computer and use it in GitHub Desktop.

Select an option

Save joshuafredrickson/285a3b3da8218529e296e7052cc49a11 to your computer and use it in GitHub Desktop.
Phone number swap based on query parameter
/*
CONFIGURE HERE
- TARGET_TEXTS: all text variants of the phone number on your site to replace.
- TARGET_TEL: digits-only version of the phone number (for tel: links).
- CAMPAIGN_NUMBERS: map of se_campaign value => replacement number display format (tel is auto-generated).
- STORAGE_KEY/STORAGE_DAYS: storage settings (uses localStorage if available, cookie fallback).
*/
(function () {
var TARGET_TEXTS = [
"(555) 555-1212",
];
var TARGET_TEL = "5555551212";
var CAMPAIGN_NUMBERS = {
// Example mappings — edit to yours (case insensitive)
// se_campaign=google => number below
yelp: "555-123-4567",
google: "555-456-7890",
};
var STORAGE_KEY = "se_campaign";
var STORAGE_DAYS = 30;
// ---------------- no edits needed below ----------------
/**
* Extracts a query parameter value from the current URL
* @param {string} name - The parameter name to look for
* @returns {string|null} The parameter value or null if not found
*/
function getQueryParam(name) {
var params = window.location.search.substring(1).split("&");
for (var i = 0; i < params.length; i++) {
var pair = params[i].split("=");
if (decodeURIComponent(pair[0]) === name) {
return typeof pair[1] === "undefined" ? "" : decodeURIComponent(pair[1] || "");
}
}
return null;
}
/**
* Tests if localStorage is available and working
* @returns {boolean} True if localStorage is supported
*/
function supportsLocalStorage() {
try {
var k = "__test__";
window.localStorage.setItem(k, "1");
window.localStorage.removeItem(k);
return true;
} catch (e) {
return false;
}
}
/**
* Sets a cookie with an expiration date
* @param {string} name - Cookie name
* @param {string} value - Cookie value
* @param {number} days - Number of days until expiration
*/
function setCookie(name, value, days) {
var expires = "";
if (days) {
var d = new Date();
d.setTime(d.getTime() + days * 24 * 60 * 60 * 1000);
expires = "; expires=" + d.toUTCString();
}
document.cookie = name + "=" + encodeURIComponent(value) + expires + "; path=/; SameSite=Lax";
}
/**
* Retrieves a cookie value by name
* @param {string} name - Cookie name to retrieve
* @returns {string|null} The cookie value or null if not found
*/
function getCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(";");
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(nameEQ) === 0) return decodeURIComponent(c.substring(nameEQ.length));
}
return null;
}
/**
* Saves the phone number configuration to storage (localStorage or cookie fallback)
* Includes expiry timestamp for automatic cleanup
* @param {Object} obj - Object with display and tel properties
*/
function saveNumberConfig(obj) {
if (supportsLocalStorage()) {
try {
var dataWithExpiry = {
data: obj,
expiry: new Date().getTime() + (STORAGE_DAYS * 24 * 60 * 60 * 1000)
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(dataWithExpiry));
} catch (e) {}
} else {
setCookie(STORAGE_KEY, JSON.stringify(obj), STORAGE_DAYS);
}
}
/**
* Loads the phone number configuration from storage
* Automatically removes expired entries
* @returns {Object|null} The stored config object or null if not found/expired
*/
function loadNumberConfig() {
var raw = null;
if (supportsLocalStorage()) {
try {
raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
var parsed = JSON.parse(raw);
// Check if it has expiry field (new format)
if (parsed && parsed.expiry) {
if (new Date().getTime() > parsed.expiry) {
// Expired, remove it
localStorage.removeItem(STORAGE_KEY);
return null;
}
return parsed.data;
}
// Old format without expiry, return as-is
return parsed;
}
} catch (e) {
return null;
}
}
// Fallback to cookie
raw = getCookie(STORAGE_KEY);
if (!raw) return null;
try {
return JSON.parse(raw);
} catch (e) {
return null;
}
}
/**
* Escapes special regex characters in a string for safe use in RegExp
* @param {string} str - String to escape
* @returns {string} Escaped string
*/
function escapeForRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/**
* Strips all non-digit characters from a string
* @param {string} str - String to normalize
* @returns {string} String containing only digits
*/
function normalizeDigits(str) {
return String(str || "").replace(/\D+/g, "");
}
var targetTextRegex = new RegExp("(" + TARGET_TEXTS.map(escapeForRegex).join("|") + ")", "g");
var targetTelDigits = normalizeDigits(TARGET_TEL);
/**
* Checks if a text node contains any target phone numbers to replace
* @param {string} text - Text content to check
* @returns {boolean} True if text contains a target phone number
*/
function shouldReplaceTextNode(text) {
targetTextRegex.lastIndex = 0; // Reset regex state for global flag
return targetTextRegex.test(text);
}
/**
* Replaces target phone numbers in a text node with the replacement number
* @param {Node} node - Text node to modify
* @param {string} replacementDisplay - The replacement phone number to display
*/
function replaceInTextNode(node, replacementDisplay) {
targetTextRegex.lastIndex = 0; // Reset regex state for global flag
node.nodeValue = node.nodeValue.replace(targetTextRegex, replacementDisplay);
}
/**
* Finds and replaces tel: links that match the target phone number
* Updates both href attribute and visible text
* @param {string} replacementTel - Digits-only replacement number for href
* @param {string} replacementDisplay - Formatted replacement number for display
*/
function replaceTelAnchors(replacementTel, replacementDisplay) {
var anchors = document.querySelectorAll('a[href^="tel:"]');
for (var i = 0; i < anchors.length; i++) {
var a = anchors[i];
var href = a.getAttribute("href") || "";
var digits = normalizeDigits(href);
if (digits === targetTelDigits) {
a.setAttribute("href", "tel:" + replacementTel);
// If the visible text equals one of the target variants, update it
var text = (a.textContent || "").trim();
if (TARGET_TEXTS.indexOf(text) > -1 || normalizeDigits(text) === targetTelDigits) {
a.textContent = replacementDisplay;
}
}
}
}
// Shared filter for TreeWalker to skip script/style/noscript tags
var SKIP_TAGS = { script: 1, style: 1, noscript: 1 };
var textNodeFilter = {
acceptNode: function (node) {
var p = node.parentNode;
if (!p || SKIP_TAGS[p.nodeName.toLowerCase()]) {
return NodeFilter.FILTER_REJECT;
}
return shouldReplaceTextNode(node.nodeValue) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
}
};
/**
* Performs initial replacement of all phone numbers on the page
* Replaces both text nodes and tel: links
* @param {Object} replacement - Object with display and tel properties
*/
function replaceEverywhere(replacement) {
if (!replacement || !replacement.display || !replacement.tel) return;
// Text nodes
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, textNodeFilter);
var current;
var nodesToUpdate = [];
while ((current = walker.nextNode())) {
nodesToUpdate.push(current);
}
for (var i = 0; i < nodesToUpdate.length; i++) {
replaceInTextNode(nodesToUpdate[i], replacement.display);
}
// tel: anchors
replaceTelAnchors(replacement.tel, replacement.display);
}
/**
* Sets up a MutationObserver to watch for dynamically added content
* Automatically replaces phone numbers in new DOM nodes
* @param {Object} replacement - Object with display and tel properties
*/
function setupObserver(replacement) {
if (!("MutationObserver" in window)) return;
var observer = new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
var m = mutations[i];
for (var j = 0; j < m.addedNodes.length; j++) {
var n = m.addedNodes[j];
if (n.nodeType === 3) {
// text node
if (shouldReplaceTextNode(n.nodeValue)) {
replaceInTextNode(n, replacement.display);
}
} else if (n.nodeType === 1) {
// element: scan its subtree quickly for text matches and tel anchors
try {
// Text nodes under this element
var walker = document.createTreeWalker(n, NodeFilter.SHOW_TEXT, textNodeFilter);
var node;
while ((node = walker.nextNode())) {
replaceInTextNode(node, replacement.display);
}
// tel anchors under this element
if (n.querySelectorAll) {
var anchors = n.querySelectorAll('a[href^="tel:"]');
for (var k = 0; k < anchors.length; k++) {
var a = anchors[k];
var href = a.getAttribute("href") || "";
var digits = normalizeDigits(href);
if (digits === targetTelDigits) {
a.setAttribute("href", "tel:" + replacement.tel);
var t = (a.textContent || "").trim();
if (TARGET_TEXTS.indexOf(t) > -1 || normalizeDigits(t) === targetTelDigits) {
a.textContent = replacement.display;
}
}
}
}
} catch (e) {}
}
}
}
});
observer.observe(document.documentElement || document.body, {
childList: true,
subtree: true
});
}
/**
* Builds a case-insensitive lookup map for campaign numbers (cached)
* @returns {Object} Lowercase campaign key to display number map
*/
var campaignLookup = (function() {
var lookup = {};
for (var key in CAMPAIGN_NUMBERS) {
if (CAMPAIGN_NUMBERS.hasOwnProperty(key)) {
lookup[key.toLowerCase()] = CAMPAIGN_NUMBERS[key];
}
}
return lookup;
})();
/**
* Looks up a campaign number by name (case insensitive)
* @param {string} campaign - Campaign name to look up
* @returns {Object|null} Object with display and tel properties, or null if not found
*/
function getCampaignNumber(campaign) {
if (!campaign) return null;
var display = campaignLookup[campaign.toLowerCase()];
if (!display) return null;
return {
display: display,
tel: normalizeDigits(display)
};
}
/**
* Main initialization function
* Checks for se_campaign parameter, loads stored config, and performs replacements
* Priority: stored config > new campaign parameter > no replacement
*/
function init() {
var campaign = getQueryParam("se_campaign");
var fromStorage = loadNumberConfig();
var chosen = null;
// If we already have a stored campaign, use it (don't override)
if (fromStorage && fromStorage.display && fromStorage.tel) {
chosen = fromStorage;
} else if (campaign) {
// Only set new campaign if nothing is stored
chosen = getCampaignNumber(campaign);
if (chosen) {
saveNumberConfig(chosen);
}
}
if (!chosen) return; // nothing to do
// Perform initial replacement and observe for future dynamic content
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
replaceEverywhere(chosen);
setupObserver(chosen);
});
} else {
replaceEverywhere(chosen);
setupObserver(chosen);
}
}
init();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment