I had a go at putting this together, and it is working for me in testing.
It scans all HTTP and HTTPS links in the active draft, canonicalises supported Amazon and YouTube links, unwraps standard Google redirect links, removes recognised tracking parameters, and leaves unsupported links unchanged.
I also added optional toggles for each cleaning rule. The selected settings are saved locally, so the action remembers them the next time it runs. The debug-log option can be enabled while testing to create a separate draft showing each link and what happened to it.
Add a Script action step and paste in the following JavaScript:
/**
* SmartLinkCleaner v1.4.2 (Interactive Edition with Persistence)
* Automatically canonicalizes popular URLs (Amazon, YouTube, Google Redirects)
* and strips tracking parameters from standard web links in the active Drafts editor.
*/
(() => {
"use strict";
// --- Load Persistent Preferences via Drafts FileManager ---
const fm = FileManager.createLocal();
const prefPath = "/SmartLinkCleaner_Prefs.json";
let savedPrefs = {};
try {
savedPrefs = fm.readJSON(prefPath) || {};
} catch (e) {
savedPrefs = {};
}
const getPref = (key, defaultVal) => {
return savedPrefs[key] !== undefined ? savedPrefs[key] : defaultVal;
};
// --- Interactive Prompt UI for Toggles ---
const p = Prompt.create();
p.title = "SmartLinkCleaner Options";
p.message = "Select which cleaning rules to apply to this draft:";
p.addSwitch("cleanGoogle", "Unwrap Google Redirects", getPref("cleanGoogle", true));
p.addSwitch("cleanAmazon", "Canonicalize Amazon URLs", getPref("cleanAmazon", true));
p.addSwitch("cleanYouTube", "Canonicalize YouTube URLs", getPref("cleanYouTube", true));
p.addSwitch("removeEmpty", "Remove Empty Query Marks (?)", getPref("removeEmpty", true));
p.addSwitch("stripTracking", "Strip Tracking Parameters", getPref("stripTracking", true));
p.addSwitch("debug", "Create Debug Log Draft", getPref("debug", true));
// Fixed prompt cancel/default button behavior
p.addButton("Run Cleaner", null, true);
if (!p.show()) {
return; // User cancelled, exit script cleanly
}
// Map prompt field values into CONFIG flags and save them persistently
const CONFIG = {
cleanGoogle: p.fieldValues["cleanGoogle"],
cleanAmazon: p.fieldValues["cleanAmazon"],
cleanYouTube: p.fieldValues["cleanYouTube"],
removeEmpty: p.fieldValues["removeEmpty"],
stripTracking: p.fieldValues["stripTracking"],
debug: p.fieldValues["debug"]
};
fm.writeJSON(prefPath, CONFIG);
const content = editor.getText();
if (!content || content.length === 0) {
alert("SmartLinkCleaner: The current draft is empty.");
return;
}
let foundCount = 0;
let stats = {
amazon: 0,
youtube: 0,
googleRedirects: 0,
trackingCleaned: 0,
emptyQueries: 0
};
let debugLog = [];
// --- Cleaners & Transformers ---
function cleanGoogleRedirect(workingURL, fragment, trailingPunctuation, rawURL) {
if (!CONFIG.cleanGoogle) return null;
// Improved global Google domain detection
if (/^https?:\/\/(?:www\.)?google\.[^/]+\/url\?/i.test(workingURL)) {
if (workingURL.includes("q=")) {
const qMatch = workingURL.match(/[?&]q=([^&]+)/i);
if (qMatch && qMatch[1]) {
try {
let unwrapped = decodeURIComponent(qMatch[1]);
if (/^https?:\/\//i.test(unwrapped) && !unwrapped.includes("...")) {
const cleaned = unwrapped + fragment + trailingPunctuation;
if (cleaned !== rawURL) {
return { cleaned, log: `Google redirect unwrapped:\n${rawURL}\n -> ${cleaned}`, type: "googleRedirects" };
}
}
} catch (e) {}
}
}
}
return null;
}
function cleanAmazon(workingURL, fragment, trailingPunctuation, rawURL) {
if (!CONFIG.cleanAmazon) return null;
const lowerRaw = workingURL.toLowerCase();
if (/(^|\.)amazon\./i.test(lowerRaw) || /(^|\.)amzn\./i.test(lowerRaw)) {
const asinMatch = workingURL.match(/\/(?:dp|gp\/product|gp\/aw\/d|exec\/obidos\/ASIN)\/([A-Z0-9]{10})/i) ||
workingURL.match(/\b([A-Z0-9]{10})\b/);
if (asinMatch) {
const asin = (asinMatch[1] || asinMatch[0]).toUpperCase();
// Preserve international Amazon stores (e.g., .co.uk, .com.au, .de)
let host = "www.amazon.com";
const hostMatch = workingURL.match(/^https?:\/\/([^/]+)/i);
if (hostMatch && hostMatch[1] && !hostMatch[1].includes("...")) {
host = hostMatch[1];
}
const cleaned = `https://${host}/dp/${asin}${fragment}${trailingPunctuation}`;
if (cleaned !== rawURL) {
return { cleaned, log: `Amazon canonicalised:\n${rawURL}\n -> ${cleaned}`, type: "amazon" };
}
}
}
return null;
}
function cleanYouTube(workingURL, fragment, trailingPunctuation, rawURL) {
if (!CONFIG.cleanYouTube) return null;
const lowerRaw = workingURL.toLowerCase();
if (/(^|\.)youtube\.com/i.test(lowerRaw) || /(^|\.)youtu\.be/i.test(lowerRaw)) {
const ytMatch = workingURL.match(/(?:v=|\/)([a-zA-Z0-9_-]{11})/);
if (ytMatch) {
const videoId = ytMatch[1];
let cleaned = `https://www.youtube.com/watch?v=${videoId}`;
const listMatch = workingURL.match(/[?&]list=([a-zA-Z0-9_-]+)/i);
if (listMatch && listMatch[1]) {
cleaned += `&list=${listMatch[1]}`;
}
cleaned += fragment + trailingPunctuation;
if (cleaned !== rawURL) {
return { cleaned, log: `YouTube canonicalised:\n${rawURL}\n -> ${cleaned}`, type: "youtube" };
}
}
}
return null;
}
function removeEmptyQuery(workingURL, fragment, trailingPunctuation, rawURL) {
if (!CONFIG.removeEmpty) return null;
if (workingURL.endsWith("?")) {
const base = workingURL.slice(0, -1);
const cleaned = base + fragment + trailingPunctuation;
if (cleaned !== rawURL) {
return { cleaned, log: `Empty question mark removed:\n${rawURL}\n -> ${cleaned}`, type: "emptyQueries" };
}
}
return null;
}
function stripTracking(workingURL, fragment, trailingPunctuation, rawURL) {
if (!CONFIG.stripTracking) return null;
if (workingURL.includes("?")) {
const parts = workingURL.split("?");
let baseUrl = parts[0];
let queryString = parts[1];
if (queryString) {
const params = queryString.split("&").filter(param => {
const p = param.toLowerCase();
return !(
// Marketing & Analytics Parameters
p.startsWith("utm_") ||
p.startsWith("fbclid=") ||
p.startsWith("igshid=") ||
p.startsWith("gclid=") ||
p.startsWith("msclkid=") ||
p.startsWith("mc_cid=") ||
p.startsWith("mc_eid=") ||
p.startsWith("yclid=") ||
p.startsWith("vero_id=") ||
p.startsWith("_hsenc=") ||
p.startsWith("_hsmi=") ||
// Affiliate & Referral Parameters
p.startsWith("mkevt=") ||
p.startsWith("mkcid=") ||
p.startsWith("mkrid=") ||
p.startsWith("campid=") ||
p.startsWith("toolid=") ||
p.startsWith("customid=") ||
p.startsWith("coliid=") ||
p.startsWith("colid=") ||
p.startsWith("psc=") ||
p.startsWith("ref_=") ||
p.startsWith("ref=") ||
// Platform-Specific Sharing Parameters
p.startsWith("s=") ||
p.startsWith("t=") ||
p.startsWith("mib2id=") ||
p.startsWith("si=") ||
p.startsWith("nd=") ||
p.startsWith("dl_branch=") ||
p.startsWith("spm=") ||
p.startsWith("algo_pvid=") ||
p.startsWith("algo_exp_id=") ||
p.startsWith("at_medium=") ||
p.startsWith("at_campaign=") ||
p.startsWith("at_custom1=") ||
p.startsWith("at_custom2=") ||
p.startsWith("start_radio=") ||
p.startsWith("ab_channel=") ||
p.startsWith("source=")
);
});
let reconstructed = baseUrl;
if (params.length > 0) {
reconstructed += "?" + params.join("&");
}
const cleaned = reconstructed + fragment + trailingPunctuation;
if (cleaned !== rawURL) {
let removedParams = queryString.split("&").length - params.length;
return { cleaned, log: `Tracking removed (${removedParams} parameter(s)):\n${rawURL}\n -> ${cleaned}`, type: "trackingCleaned" };
}
}
}
return null;
}
// --- Main Execution Loop ---
const updatedContent = content.replace(/https?:\/\/[^\s<>"“”‘’]+/gi, rawURL => {
foundCount++;
let fragment = "";
let workingURL = rawURL;
const hashIndex = workingURL.indexOf("#");
if (hashIndex !== -1) {
fragment = workingURL.substring(hashIndex);
workingURL = workingURL.substring(0, hashIndex);
}
const trailingMatch = workingURL.match(/[.,;:!…]+$/);
const trailingPunctuation = trailingMatch ? trailingMatch[0] : "";
let cleanCandidate = workingURL.replace(/[.,;:!…]+$/, "");
let result =
cleanGoogleRedirect(cleanCandidate, fragment, trailingPunctuation, rawURL) ||
cleanAmazon(cleanCandidate, fragment, trailingPunctuation, rawURL) ||
cleanYouTube(cleanCandidate, fragment, trailingPunctuation, rawURL) ||
removeEmptyQuery(cleanCandidate, fragment, trailingPunctuation, rawURL) ||
stripTracking(cleanCandidate, fragment, trailingPunctuation, rawURL);
if (result) {
stats[result.type]++;
debugLog.push(result.log);
return result.cleaned;
}
debugLog.push(`Unchanged/Passed:\n${rawURL}`);
return rawURL;
});
const totalChanged = stats.amazon + stats.youtube + stats.googleRedirects + stats.trackingCleaned + stats.emptyQueries;
// Only update and save if the content actually changed
if (updatedContent !== content) {
editor.setText(updatedContent);
editor.save();
}
// Only create a debug log if debugging is active AND changes were actually made
if (CONFIG.debug && totalChanged > 0 && debugLog.length > 0) {
const logDraft = Draft.create();
logDraft.content = "--- SmartLinkCleaner Log ---\n\n" + debugLog.join("\n\n");
logDraft.update();
editor.load(logDraft);
}
// Summary Notification
const alertMessage =
`SmartLinkCleaner\n` +
`• URLs scanned: ${foundCount}\n` +
`• Amazon canonicalised: ${stats.amazon}\n` +
`• YouTube canonicalised: ${stats.youtube}\n` +
`• Google redirects unwrapped: ${stats.googleRedirects}\n` +
`• Tracking parameters removed: ${stats.trackingCleaned}\n` +
`• Empty queries removed: ${stats.emptyQueries}` +
(CONFIG.debug && totalChanged > 0 ? `\n\n(Log draft created)` : ``);
alert(alertMessage);
})();