Clean Links + Copy (no UTM)

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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 });
})();