Каждый день мы делимся ссылками в мессенджерах, копируем их из браузера или кликаем по ним. И почти всегда к реальному адресу прилипает хвост из UTM-меток, fbclid, gclid и прочего мусора, которым маркетологи и платформы помечают наш трафик.

Создать расширение, которое отрезает этот хвост с помощью регулярного выражения — задача на 10 минут. Но сделать так, чтобы это расширение не сломало работу сайтов, не убило поисковики и не вызвало бесконечные циклы переадресации в современных реалиях Manifest V3 — уже интереснее.

Недавно я завершил работу над расширением ZeroTail и хочу поделиться несколькими неочевидными подводными камнями, с которыми столкнулся при реализации авто-очистки URL.

Проблема №1: Слепая чистка ломает поиск

Первое желание — просто вырезать все известные параметры из URL.searchParams у любой ссылки. Но если мы применим это правило ко всем сайтам без разбора, мы сломаем поисковики.

Например, Google использует параметры source и medium для сортировки результатов поиска (кнопки "Картинки", "Видео" и т.д.). Если мы их вырежем, поиск превратится в тыкву.

Решение: Я ввел жесткий whitelist (белый список) для доменов, где очистка отключена на корневом уровне.

const WHITELIST = [
  'google.com', 'bing.com', 'yandex.ru', 'duckduckgo.com' 
];

async function cleanUrl(url) {
  const urlObj = new URL(url);
  if (WHITELIST.some(site => urlObj.hostname.includes(site))) {
    return url; // Не трогаем поисковики
  }
  // ... логика очистки
}

Проблема №2: Бесконечные редиректы и сломанные XHR

В эпоху Manifest V2 фоновой скрипт мог перехватить любой webRequest и изменить redirectUrl. В Manifest V3 мы ограничены declarativeNetRequest (которая не умеет динамически парсить параметры строк) или событием webNavigation.onBeforeNavigate.

Если просто слушать onBeforeNavigate и делать chrome.tabs.update({ url: cleanedUrl }), мы наступим на грабли:

  1. Сайт сделает POST-запрос (например, отправка формы) — мы его сломаем.

  2. Сайт сделает XHR/Fetch запрос за JSON — мы обрушим JS.

  3. Мы сделаем редирект на очищенный URL, что снова вызовет onBeforeNavigate, и мы улетим в бесконечный цикл.

Решение: Проверять transitionType и типы схем. Нам нужно реагировать только на классические клики по ссылкам.

chrome.webNavigation.onBeforeNavigate.addListener(async (details) => {
  // Получаем настройки (функция авто-очистки может быть выключена)
  const result = await chrome.storage.sync.get(['autoClean']);
  if (!result.autoClean) return;

  // ВАЖНО: реагируем только на переходы по ссылкам (link), 
  // игнорируем typing, auto_subframe, form_submit и т.д.
  if (details.transitionType !== 'link') return;

  const cleanedUrl = await cleanUrl(details.url);

  // Если URL изменился — перенаправляем
  if (details.url !== cleanedUrl) {
    chrome.tabs.update(details.tabId, { url: cleanedUrl });
  }
}, {
  url: [
    {hostContains: '.'},
    {schemes: ['http', 'https']}
  ]
});

Примечание: фильтр hostContains: '.' нужен, чтобы не пытаться чистить внутренние страницы расширения (например, chrome-extension://...), так как new URL() с такими схемами в контексте веб-страницы может вести себя непредсказуемо.

Проблема №3: Как красиво скопировать ссылку без лишних API

Иногда авто-очистка не нужна (например, вы хотите отправить грязную ссылку коллеге, чтобы он увидел, откуда вы пришли, но сами хотите скопировать её чистую версию). Для этого я добавил пункт в контекстное меню браузера.

В Manifest V3 нельзя напрямую писать в буфер обмена из фонового Service Worker'а. Нужно делегировать это Content Script'у на странице.

В background.js вызываем контекстное меню:

chrome.contextMenus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === "cleanCopy" && info.linkUrl) {
    chrome.tabs.sendMessage(tab.id, {
      action: "cleanAndCopy",
      url: info.linkUrl
    });
  }
});

А в content.js используем современный Async Clipboard API с минимальным и ненавязчивым UI (вместо тяжелых алертов):

function cleanAndCopyLink(url) {
  chrome.runtime.sendMessage({ action: "cleanLink", url: url }, (response) => {
    if (response && response.cleanedUrl) {
      navigator.clipboard.writeText(response.cleanedUrl).then(() => {
        showNotification(`Ссылка очищена и скопирована!\nУдалено трекеров: ${response.trackerCount}`);
      });
    }
  });
}

function showNotification(message) {
  let toast = document.getElementById('zerotail-toast');
  if (!toast) {
    toast = document.createElement('div');
    toast.id = 'zerotail-toast';
    // Стилизация под Material Design / современные тосты
    toast.style.cssText = `
      position: fixed; bottom: 20px; right: 20px;
      background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
      color: white; padding: 12px 20px; border-radius: 8px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 999999;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      max-width: 320px; transition: opacity 0.3s ease;
    `;
    document.body.appendChild(toast);
  }
  toast.textContent = message;
  toast.style.opacity = '1';
  setTimeout(() => { toast.style.opacity = '0'; }, 3000);
}

Итоги

Написание "простого" парсера ссылок в 2026 году требует учета специфики Service Worker'ов в Manifest V3 и бережного отношения к навигационным событиям браузера. Ошибки в transitionType или отсутствие whitelist'ов мгновенно превращают полезный инструмент в источник головной боли для пользователя.

P.S. Описанная выше архитектура реализована в виде готового расширения ZeroTail для Chrome. Код на данный момент закрыт, но расширение абсолютно бесплатное, не требует регистрации и собирает ноль данных о пользователях. В нем есть как авто-очистка при переходе, так и ручное управление белыми/черными списками параметров.

https://chromewebstore.google.com/detail/zerotail/kmdgihkhibbfhcjlaejjnpfimkfmhmke?utm_source=ext_app_menu