Реализация маппинга 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 рабочих дня.







