Опубліковано: · Оновлено: · автор: kodewright
Масове URL-кодування для SEO: карта редиректів (2026)
Масове URL-кодування для карт редиректів, UTM-міток і кириличних слагів: який енкодер обрати і як знайти %25 та розбіжність NFC/NFD до того, як буде 404.
Масове URL-кодування для SEO зводиться до одного правила: кодуйте кожен рядок карти редиректів чи списку UTM-міток однаково, правильною функцією для кожної частини URL, і перед запуском перевіряйте результат на подвійне кодування. Зламана карта редиректів майже завжди впирається в одне з чотирьох: не той енкодер, %2520, плутанина + і %20 або два різні написання того самого не-ASCII слагу.
Чесно скажу про свій досвід: великої міграції з редиректами я сам не проводив. Зате все нижче можна відтворити: кожен вивід я запускав у Node 22.23 або брав прямо зі специфікації, тож ви можете вставити ті самі рядки в консоль браузера і перевірити.
Енкодер обирають для частини URL, а не для файлу
У JavaScript є два енкодери, encodeURIComponent() і encodeURI(), а HTML-форми додають третю домовленість. Вони екранують різні набори символів, і застосувати один із них не до того фрагмента — найпоширеніший спосіб зламати карту редиректів.
| Режим | Кодує | Залишає як є | Для чого |
|---|---|---|---|
encodeURIComponent() | Усе, крім A-Z a-z 0-9 - _ . ! ~ * ' ( ) | Структура URL не зберігається | Одне значення: параметр запиту, UTM-мітка, сегмент шляху |
encodeURI() | Пробіли, не-ASCII, % і ще кілька символів | ; , / ? : @ & = + $ # і символи з рядка вище | Повний URL для href або заголовка Location |
Форма (application/x-www-form-urlencoded) | Як encodeURIComponent(), але пробіл стає + | Літери, цифри, * - . _ | Тіла HTML-форм і рядки запиту, які читає парсер форм |
Апостроф не кодує жодна з функцій JavaScript. У багатьох шпаргалках men's перетворюється на men%27s, але так роблять URLSearchParams або new URL() у рядку запиту. Обидва написання коректні, і це різні рядки: правило редиректу з точним збігом, записане одним способом, не спрацює для іншого. Оберіть одну домовленість на всю карту.
Класична помилка в саморобних скриптах редиректів — прогнати весь URL через encodeURIComponent(). Він екранує і структуру, тож https://example.com/page стає https%3A%2F%2Fexample.com%2Fpage, а це вже не URL.
У моєму URL кодувальнику / декодувальнику ці три режими винесено окремо (Component, Full URL, Form +), а опція «Encode line by line» обробляє кожен рядок незалежно. Усе працює у вкладці браузера: карта, яку ви вставили, нікуди не надсилається.
UTM-мітки: один сирий & ділить кампанію надвоє
Назві кампанії на кшталт Black Friday & Cyber Monday 🔥 потрібен режим Component, бо сирий & завершує параметр. new URL() прочитає utm_campaign як Black Friday , а решту зробить окремим параметром без назви. Правильно закодований варіант:
Black%20Friday%20%26%20Cyber%20Monday%20%F0%9F%94%A5
Посилання, зібрані різними інструментами (рекламний кабінет, формула в таблиці, набране вручну), часто кодують ту саму назву по-різному, і так одна кампанія розпадається на кілька рядків у звіті аналітики. Кодуйте значення один раз, там, де створюєте посилання, а далі копіюйте готовий результат.
Подвійне кодування: чому %2520 — найчастіший 404
Ось реальний вивід encodeURIComponent() для значення, закодованого двічі:
Original value: Men's Shoes
Encoded once: Men's%20Shoes correct (the apostrophe stays as is)
Encoded again: Men's%2520Shoes wrong: %25 is a literal "%"
Значення закодували і зберегли, а потім інший скрипт (або той самий, запущений вдруге) закодував його ще раз. Другий прохід екранує знаки %, бо % — єдиний символ, який обидві функції екранують завжди. Синтаксично все правильно, але адреса веде в нікуди: сервер віддає 404, а в Search Console URL потрапляє в «Не знайдено (404)» у звіті про індексування сторінок замість того, щоб передати сигнали новій сторінці. encodeURI() поводиться так само, тому повний URL, закодований двічі, теж дає %2520.
Перевірка для аудиту — один пошук: %25, за яким ідуть дві шістнадцяткові цифри. Якщо в URL насправді немає знака відсотка, кожен збіг означає двічі закодований рядок.
Кириличні слаги: чому на цьому сайті я використовую латиницю
Рекомендації Google щодо структури URL радять використовувати слова мовою аудиторії, а не-ASCII символи в посиланнях кодувати percent-кодуванням. Тобто кириличний слаг сам по собі не штраф для SEO. Я однаково їх не використовую: українські сторінки цього сайту мають англійський слаг (/uk/blog/<той-самий-слаг>), і причина в тому, що відбувається з кириличним шляхом, щойно він виходить за межі адресного рядка.
Той самий заголовок статті у вигляді трьох можливих слагів, пропущених через new URL() у Node 22.23:
| Слаг | Набрано символів | Символів у запиті |
|---|---|---|
/uk/blog/як-створити-надійний-пароль | 36 | 156 |
/uk/blog/yak-stvoryty-nadiinyi-parol (транслітерація) | 36 | 36 |
/uk/blog/how-to-create-a-strong-password (англійською) | 40 | 40 |
Кожна кирилична літера займає два байти UTF-8, тож перетворюється на шість символів екранування на зразок %D1%8F. Саме ці 156 символів ви побачите, коли вставите посилання в месенджер чи поштовий клієнт, який його не декодує, а також у логах сервера, у більшості експортів краулерів і в карті редиректів. Прочитати чи порівняти таке на око неможливо.
Друга проблема тонша, і в карті редиректів я переживав би саме через неї. В українській є дві літери, які Unicode може зберігати двома способами: й і ї бувають одним готовим кодовим пунктом (NFC) або базовою літерою з комбінованим діакритичним знаком (NFD). Виглядають вони однаково, а кодуються по-різному:
encodeURIComponent('й') // '%D0%B9'
encodeURIComponent('й'.normalize('NFD')) // '%D0%B8%CC%86' (и + combining breve)
encodeURIComponent('ї'.normalize('NFD')) // '%D1%96%CC%88' (і + combining diaeresis)
Текст, скопійований з одного редактора, може бути в NFC, а те саме слово, набране в іншій системі, вийде в NFD. Для правила редиректу з точним збігом надійний у NFC і в NFD — дві не пов'язані адреси. Якщо ви все ж використовуєте кириличні слаги, пропускайте кожен рядок через .normalize('NFC') перед кодуванням. Мій кодувальник нормалізацію не робить, тож зробіть її заздалегідь.
Третя пастка — регістр. За RFC 3986 %d0%b9 і %D0%B9 рівнозначні, але new URL('https://example.com/%d0%b9').pathname повертає /%d0%b9 без змін, а grep, VLOOKUP у таблиці чи правило CDN порівнюють сирий рядок. Нормалізована форма — шістнадцяткові цифри великими літерами, тож зведіть усе до неї.
Якщо транслітеруєте, тримайтеся однієї системи. Для української офіційна — таблиця Кабінету Міністрів 2010 року, та сама, що для імен у паспортах: я на початку слова стає ya, в інших позиціях ia; и стає y; й усередині слова стає i; м'який знак не передається. Так як створити надійний пароль перетворюється на yak-stvoryty-nadiinyi-parol. Змішування систем (nadiynyi на одній сторінці, nadiinyi на іншій) повертає ту саму проблему точного збігу, тільки вже в ASCII.
Для доменів діє інше правило: інтернаціоналізоване ім'я хоста перетворюється на Punycode, а не на percent-екранування. new URL('https://приклад.укр/пароль').href повертає https://xn--80aikifvh.xn--j1amh/%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C.
Шпаргалка із зарезервованих символів
Символи зі структурним значенням — це зарезервований набір із RFC 3986 §2.2. Усередині значення їх кодують, а там, де вони є структурою, не чіпають. Пробіл і % до зарезервованих не належать: пробіл заборонений будь-де, а % і є символом екранування. Останні дві колонки перевірено в Node 22.
| Символ | Роль в URL | Percent-кодування | encodeURIComponent() | encodeURI() |
|---|---|---|---|---|
(пробіл) | Заборонений | %20 | кодує | кодує |
& | Роздільник параметрів запиту | %26 | кодує | залишає |
= | Роздільник ключа і значення | %3D | кодує | залишає |
? | Початок рядка запиту | %3F | кодує | залишає |
# | Початок фрагмента | %23 | кодує | залишає |
/ | Роздільник сегментів шляху | %2F | кодує | залишає |
+ | Підроздільник; пробіл у кодуванні форм | %2B | кодує | залишає |
% | Символ екранування | %25 | кодує | кодує |
' | Підроздільник | %27 | залишає | залишає |
Особливо стежте за +: encodeURI() його залишає, а парсер форм на стороні отримувача прочитає його як пробіл.
Пакетна обробка карти редиректів на 500 рядків
Спокуслива схема «спершу все декодувати, потім усе один раз закодувати» ламається на кожному рядку, де вже є екранований зарезервований символ. Декодування в режимі Full URL залишає %26, а кодування в тому ж режимі потім екранує його %. Декодування в режимі Component повертає %26 назад у &, і той стає роздільником:
Input: /search?q=tom%26jerry%20show
Full URL decode: /search?q=tom%26jerry show
then encode: /search?q=tom%2526jerry%20show double-encoded
Component decode: /search?q=tom&jerry show
then encode: /search?q=tom&jerry%20show & is now a separator
Порядок, який працює:
- Якщо в колонці адрес призначення є не-ASCII текст, нормалізуйте її до NFC.
- Спершу знайдіть двічі закодовані рядки (пошук
%25). Декодуйте їх один раз у режимі Full URL, щоб зняти зайвий шар, і перевірте результат. - Рядки із сирими пробілами чи не-ASCII символами і без жодного
%закодуйте в режимі Full URL з увімкненою опцією «Encode line by line». - Рядки, де вже наявні екранування змішано із сирими символами, не проганяйте через кодування повного URL: воно ще раз екранує наявні
%. Закодуйте тільки зламане значення в режимі Component або нормалізуйте рядок парсером URL з WHATWG (нижче). - Переведіть шістнадцяткові цифри в усіх екрануваннях у верхній регістр і порівняйте результат із вхідними даними. Рядки без видимих змін виправляти не було чого.
- Перенесіть очищену колонку в правила редиректів (Nginx
rewrite, Apache.htaccessабо edge-правила вашого CDN).
Парсер із кроку 4 працює в Node 18+ або в консолі браузера. Він кодує сирі символи і не чіпає наявні екранування, але намірів не вгадує: сирий & так і залишиться роздільником.
new URL("https://example.com/a%20b/c d?q=tom%26jerry show").href
// 'https://example.com/a%20b/c%20d?q=tom%26jerry%20show'
У зворотний бік це працює так само, і це зручно, коли експорт краулера від клієнта приходить у percent-кодуванні, а для контент-аудиту потрібні читабельні шляхи. Рядок з одиноким % (скажімо, 100%) — некоректне percent-кодування: decodeURIComponent() кидає URIError: URI malformed. У моєму інструменті один такий рядок зупиняє всю пачку з помилкою «Invalid input», а не пропускається, тож спершу знайдіть %, після якого не йдуть дві шістнадцяткові цифри, і виправте ці рядки.
Кодування нічого не приховує
Percent-кодування не захищає значення. Будь-хто розверне його одним викликом функції, так само як Base64. Якщо в UTM-параметрі чи адресі редиректу є щось, чого не варто показувати (внутрішня кодова назва кампанії, слаг ще не анонсованого продукту), кодування нічого не змінить: тримайте це поза публічними URL. Де саме проходить межа між кодуванням, шифруванням і хешуванням, я розбирав у статті кодування, шифрування і хешування.
Те саме з ідентифікаторами. Якщо кожне правило чи кампанія в карті отримує унікальний ID, не будуйте його з Date.now(), бо пакетний скрипт створює багато рядків за ту саму мілісекунду, і не беріть кілька символів з Math.random(): він передбачуваний і дає колізії значно раніше, ніж зазвичай думають. UUID версії 4 з crypto.randomUUID() містить 122 випадкові біти з криптографічного генератора браузера. Генератор UUID використовує саме цей виклик і створює до 100 UUID за раз.
Перед запуском редиректів
Проженіть колонку адрес призначення в построковому режимі, пошукайте у виводі %25 і шістнадцяткові цифри в нижньому регістрі, а потім вибірково перевірте з десяток редиректів командою curl -sI <old-url>: вона покаже точний заголовок Location, який віддає сервер, а саме за ним іде Googlebot. Якщо параметр не схожий ні на звичайний текст, ні на percent-кодування (довга послідовність літер і цифр, часто з = у кінці), найімовірніше, це Base64, і Base64 декодувальник покаже, що всередині.
FAQ
Пробіл в URL — це %20 чи +?
Використовуйте %20. У шляху + означає саме знак плюс. У рядку запиту парсери форм (URLSearchParams, PHP, parse_qs у Python) читають + як пробіл, а decodeURIComponent() залишає його плюсом. %20 будь-який парсер прочитає як пробіл.
%D0%B9 і %d0%b9 — це той самий URL?
За RFC 3986 шістнадцяткові цифри в percent-escape не залежать від регістру, тож обидва варіанти декодуються в ту саму літеру (й). Але інструменти з точним збігом нічого не декодують: правило редиректу, grep по логах чи пошук у таблиці бачать два різні рядки. Нормалізована форма — великі літери, тож зведіть усе до неї.
Чи безпечно вставляти карту редиректів клієнта в онлайн-кодувальник URL?
Лише тоді, коли інструмент усе робить у браузері. Карта редиректів часто розкриває ще не опубліковану структуру URL, назви кампаній і внутрішні шляхи. Відкрийте вкладку Network у DevTools і вставте текст: клієнтський інструмент не надішле жодного запиту.