Clean Links + Copy (no UTM)

Автоочистка ссылок от UTM/трекеров и быстрая копия чистого URL (кнопка 🔗 рядом со ссылкой + меню).

// ==UserScript==
// @name         Clean Links + Copy (no UTM)
// @namespace    https://nikk.agency/
// @version      1.0.0
// @description  Автоочистка ссылок от UTM/трекеров и быстрая копия чистого URL (кнопка 🔗 рядом со ссылкой + меню).
// @author       NAnews / NiKK
// @license      MIT
// @match        *://*/*
// @run-at       document-idle
// @grant        GM_setClipboard
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function () {
  "use strict";

  const TRACKING_PARAMS = new Set([
    // универсальные
    "utm_source","utm_medium","utm_campaign","utm_term","utm_content","utm_name","utm_id","utm_reader","utm_brand",
    // соцсети/рекламные
    "fbclid","gclid","wbraid","gbraid","yclid","mc_cid","mc_eid","igshid","si","spm",
    "ref","ref_src","ref_url","campaign_id","adset_id","ad_id",
    // прочие популярные
    "mkt_tok","vero_id","sca_esv","_hsenc","_hsmi","ncid","trk","rb_clickid","ttclid",
  ]);

  const BUTTON_CLASS = "clean-link-copy-btn";

  function cleanUrl(raw) {
    try {
      const url = new URL(raw, location.href);
      // чистим хеш-трекинг типа ?x#~:text=...
      if (url.hash && /~:text=/.test(url.hash)) url.hash = "";
      // чистим параметры
      const p = url.searchParams;
      // удаляем все utm_*
      [...p.keys()].forEach((k) => {
        if (k.startsWith("utm_") || TRACKING_PARAMS.has(k)) p.delete(k);
      });
      // если остались пустые search/hash — норм
      url.search = p.toString() ? "?" + p.toString() : "";
      return url.toString();
    } catch {
      return raw;
    }
  }

  function attachCopyButtons() {
    const links = document.querySelectorAll("a[href]:not([data-clean-processed])");
    for (const a of links) {
      a.setAttribute("data-clean-processed", "1");

      // переписываем href на чистый (не меняем видимую надпись)
      const cleaned = cleanUrl(a.href);
      if (cleaned && cleaned !== a.href) a.href = cleaned;

      // не добавляем кнопку в навигации, меню и т.п. (сокращаем шум)
      const rect = a.getBoundingClientRect();
      const isTiny = rect.width < 20 || rect.height < 12;
      if (isTiny) continue;

      const btn = document.createElement("button");
      btn.type = "button";
      btn.textContent = "🔗Copy";
      btn.title = "Скопировать чистый URL";
      btn.className = BUTTON_CLASS;
      btn.addEventListener("click", (e) => {
        e.preventDefault();
        e.stopPropagation();
        const url = cleanUrl(a.href);
        try {
          if (typeof GM_setClipboard === "function") {
            GM_setClipboard(url, { type: "text", mimetype: "text/plain" });
          } else {
            navigator.clipboard?.writeText(url);
          }
          flash(a, "Скопировано!");
        } catch {
          flash(a, "Не удалось скопировать");
        }
      });

      // обертка для позиционирования
      const wrapper = document.createElement("span");
      wrapper.style.position = "relative";
      a.parentNode.insertBefore(wrapper, a);
      wrapper.appendChild(a);
      wrapper.appendChild(btn);
    }
  }

  function flash(el, msg) {
    const note = document.createElement("span");
    note.textContent = msg;
    note.style.cssText = `
      position:absolute; z-index: 999999; top:-1.6em; right:0;
      padding:2px 6px; border-radius:6px; font:12px/1.2 system-ui, sans-serif;
      background: rgba(0,0,0,.75); color:#fff; pointer-events:none;
    `;
    el.closest("span")?.appendChild(note);
    setTimeout(() => note.remove(), 900);
  }

  // меню
  if (typeof GM_registerMenuCommand === "function") {
    GM_registerMenuCommand("Очистить все ссылки сейчас", () => {
      document.querySelectorAll("a[href]").forEach((a) => (a.href = cleanUrl(a.href)));
      alert("Готово: ссылки очищены.");
    });

    GM_registerMenuCommand("Скопировать чистый URL этой страницы", () => {
      const cleaned = cleanUrl(location.href);
      if (typeof GM_setClipboard === "function") {
        GM_setClipboard(cleaned, { type: "text", mimetype: "text/plain" });
      } else {
        navigator.clipboard?.writeText(cleaned);
      }
      alert("Скопировано:\n" + cleaned);
    });
  }

  // стили кнопки
  const css = document.createElement("style");
  css.textContent = `
    .${BUTTON_CLASS}{
      margin-left:6px; padding:2px 6px; border:1px solid rgba(0,0,0,.2);
      border-radius:6px; background:#fff; cursor:pointer; font:12px/1 system-ui,sans-serif;
      box-shadow:0 1px 2px rgba(0,0,0,.05);
    }
    .${BUTTON_CLASS}:hover{ background:#f5f5f5 }
  `;
  document.documentElement.appendChild(css);

  // первичный прогон и наблюдатель мутаций (для SPA/ленивой подгрузки)
  attachCopyButtons();
  const mo = new MutationObserver(() => attachCopyButtons());
  mo.observe(document.documentElement, { subtree: true, childList: true });
})();