Реалізація маппінгу URL (старих → нових) та налаштування 301-редиректів під час міграції
301-редиректи під час міграції — основний інструмент передачі SEO-ваги сторінок на новий сайт. Кожен втрачений URL без редиректу — це потенційно втрачений трафік та позиції у пошуку.
Створення таблиці URL-маппінгу
URL-маппінг ведеться в CSV-таблиці, яка стає єдиним джерелом істини:
old_url,new_url,status_code,priority,notes
/blog/2020/01/old-slug,/articles/old-slug,301,high,main page
/category/news,/blog/news,301,high,category page
/wp-content/uploads/img.jpg,/media/img.jpg,301,medium,media file
/contact-us,/contacts,301,high,page renamed
/product/old-name,/shop/new-name,301,high,product renamed
/old-promo-page,,410,low,deleted page
Статус 410 (Gone) для видалених сторінок сигналізує пошуковику про остаточне видалення — краще ніж 404 для сторінок, які не повернуться.
Автоматичний маппінг за slug
Якщо змінилася лише структура URL (додався/забрався префікс):
def generate_url_map(old_urls, url_transform_fn):
mapping = []
for old_url in old_urls:
new_url = url_transform_fn(old_url)
if old_url != new_url:
mapping.append({'old': old_url, 'new': new_url, 'code': 301})
return mapping
# Приклади трансформацій
def wp_to_flat(url):
# /2020/01/post-slug → /articles/post-slug
import re
match = re.match(r'^/\d{4}/\d{2}/(.+)$', url)
if match:
return f"/articles/{match.group(1)}"
return url
def add_lang_prefix(url, lang='ru'):
# /page → /ru/page
if not url.startswith(f'/{lang}/'):
return f'/{lang}{url}'
return url
Генерація nginx map
def generate_nginx_map(mapping_csv, output_file):
import csv
lines = ['# Auto-generated redirects', 'map $request_uri $redirect_target {']
lines.append(' default "";')
lines.append(' hostnames;') # включити підтримку hostname patterns
with open(mapping_csv) as f:
reader = csv.DictReader(f)
for row in reader:
old = row['old_url'].rstrip('/')
new = row['new_url']
code = row.get('status_code', '301')
if code == '410':
# Для 410 використовуємо інший map
continue
# Основний URL
lines.append(f' "~^{re.escape(old)}$" "{new}";')
# З trailing slash
if old != '/':
lines.append(f' "~^{re.escape(old)}/$" "{new}";')
lines.append('}')
with open(output_file, 'w') as f:
f.write('\n'.join(lines))
Конфігурація Nginx:
include /etc/nginx/redirect_map.conf;
server {
listen 80;
server_name site.com www.site.com;
# Обробити редиректи
if ($redirect_target != "") {
return 301 $redirect_target;
}
# 410 для видалених сторінок
location ~* ^/(old-promo|deleted-category|removed-product) {
return 410;
}
# Універсальний редирект для невідомих старих шляхів
# (обережно — може зломати новий контент)
# try_files $uri $uri/ @legacy_redirect;
}
Генерація .htaccess для Apache
def generate_htaccess(mapping_csv, output_file):
lines = [
'RewriteEngine On',
'RewriteBase /',
''
]
with open(mapping_csv) as f:
reader = csv.DictReader(f)
for row in reader:
old = row['old_url'].lstrip('/')
new = row['new_url']
code = row.get('status_code', '301')
if code == '410':
lines.append(f'RewriteRule ^{re.escape(old)}$ - [G,L]')
else:
lines.append(f'RewriteRule ^{re.escape(old)}$ {new} [R={code},L]')
with open(output_file, 'w') as f:
f.write('\n'.join(lines))
Перевірка покриття редиректів
import requests
def verify_redirects(mapping_csv, base_url):
errors = []
with open(mapping_csv) as f:
reader = csv.DictReader(f)
for row in reader:
old_url = f"{base_url}{row['old_url']}"
expected_new = row['new_url']
expected_code = int(row.get('status_code', 301))
# Перевірити лише редирект, не слідувати
resp = requests.get(old_url, allow_redirects=False)
if expected_code in (301, 302):
if resp.status_code != expected_code:
errors.append(f"Expected {expected_code}, got {resp.status_code}: {old_url}")
elif not resp.headers.get('Location', '').endswith(expected_new):
errors.append(f"Wrong redirect target: {old_url} → {resp.headers.get('Location')}, expected {expected_new}")
elif expected_code == 410:
if resp.status_code != 410:
errors.append(f"Expected 410, got {resp.status_code}: {old_url}")
return errors
Краулінг старого сайту для повного покриття
Перед налаштуванням редиректів потрібен повний список URL:
# Screaming Frog експорт всіх URL
# або wget-краулінг
wget --spider --recursive --no-verbose --output-file=crawl.log \
https://old-site.com 2>&1
grep -E "^--" crawl.log | awk '{print $3}' | sort -u > all_urls.txt
# Знайти URL з all_urls.txt, не покриті редиректами
with open('all_urls.txt') as f:
crawled_urls = {line.strip() for line in f}
with open('mapping.csv') as f:
mapped_old_urls = {row['old_url'] for row in csv.DictReader(f)}
uncovered = crawled_urls - mapped_old_urls
print(f"Uncovered URLs ({len(uncovered)}):")
for url in sorted(uncovered):
print(f" {url}")
GSC-моніторинг після запуску
Google Search Console → Coverage → Excluded → Crawled – currently not indexed
Якщо після міграції з'являється багато нових 404 — це пропущені редиректи. Сторінки з трафіком (з GSC Performance) мають бути покриті редиректами в порядку пріоритету.
Строк виконання
Створення маппінгу для сайту до 1000 URL, генерація конфіга nginx та перевірка — 2–3 робочих дні.







