InquirerMunicipal employee shot dead in Bukidnon; manhunt on for suspectsThe Jerusalem PostAfter America's midterms, the clock is ticking for Israel to bridge its political dividesDaily MaverickThe varsity research subsidy game is up — the education department must not back downPunch2026 set to be second-hottest year on record — EUZDF heuteAktuelle Pressemitteilungen des ZDF7sur7Record historique pour la Bourse belge: comment expliquer ce “score absolument phénoménal”?La PresseTransports en direct | Fermeture de deux voies sur l’A40 ouest près du boulevard Henri-BourassaSportstarAsian Para Games 2026: Meet the Indian gold medallistsMalay MailRinggit closes lower amid global market jitters and semiconductor sell-offPolsat NewsDziennikarka Polsatu ofiarą oszustów. "Jestem w szoku"01netIntel Core i9, SSD 2 To, RAM 16 Go : au prix fou de 230 €, ce PC portable se vend par camions 🚛ANSAMinacce di attentati a giornali e tv irlandesi, evacuate le redazioni a Dublino
The Daily Newsstand · Free, Always
Friday, November 6, 2026

Автоматизация SEO-аудита: проверяем редиректы, canonical и hreflang как граф

Translate

Краулер нашёл canonical, сервер вернул 200, в таблице стоит зелёная галочка. При этом canonical ведёт на редирект, языковая версия не ссылается обратно, а старые URL образуют цикл. По отдельности поля заполнены; связи между страницами сломаны.

Покажу, как превратить выгрузку обхода в воспроизводимую проверку графа: отделить неизвестные данные от ошибок, сохранить доказательства и использовать результат перед релизом. Внутри - Python-скрипт без сторонних зависимостей и синтетический пример, который можно повторить локально.

1. Сначала определяем, что именно проверяем

Автоматизация SEO-аудита полезна, когда результат проверки можно воспроизвести на тех же входных данных. Для этого нужны три отдельные сущности: снимок наблюдений, набор правил и список нарушений с адресами источника и цели.

В этой статье проверяем согласованность объявленных связей сайта. Не определяем, какие страницы находятся в индексе. Google прямо указывает, что ответ 200 не гарантирует индексацию. Поэтому поле indexed: true, вычисленное из HTTP-кода, было бы ошибкой модели данных.

Предлагаемая архитектура:

Список URL и настройки обхода
            ↓
Снимок HTTP-ответов и извлечённых аннотаций
            ↓
Граф редиректов / canonical / hreflang
            ↓
Детерминированные правила
            ↓
Нарушение + источник + цель + проверка исправления

Это инженерная схема аудита, а не описание внутреннего устройства поисковой системы.

Нужен ещё контракт ожидаемого поведения. Архивная карточка может намеренно возвращать 404; переход со старого домена на новый может быть штатным редиректом. Ошибка возникает при расхождении наблюдений с назначением страницы. Поэтому до запуска полезно разделить URL на индексируемые посадочные, служебные страницы, удалённые документы и адреса миграции.

2. Три типа связей - три разных смысла

Редирект: куда реально отправляет сервер

Сохраняйте каждый переход отдельно: исходный адрес, HTTP-код, значение Location. Если клиент автоматически прошёл цепочку и оставил только финальный 200, восстановить промежуточные узлы из такого результата нельзя.

Для Google постоянные 301/308 и временные 302/307 имеют разный смысл. Наш скрипт объединяет их только для обхода графа; решение о корректности выбранного кода остаётся отдельной проверкой миграции.

Цикл - безусловный дефект маршрута. Цепочка из двух переходов - повод проверить конфигурацию, но не доказательство потери позиций. Ниже это диагностическое правило проекта, а не «лимит Google в один редирект».

Canonical: какую версию предпочитает сайт

rel="canonical" - сигнал предпочтения канонической версии, а не команда, гарантирующая выбор адреса. Проверяем доступность объявленной цели и согласованность конфигурации. Фактический выбор Google из HTML не выводим.

Отсутствие canonical не делает любую страницу ошибочной. Для конкретного шаблона self-canonical можно включить в контракт команды, но это уже локальная политика. Полезно также отдельно искать противоречия между HTML canonical, HTTP Link и sitemap.

Hreflang: какие страницы образуют языковую группу

В документации по локализованным версиям описаны ссылки на саму страницу и альтернативы, полностью квалифицированные адреса и взаимные ссылки между версиями. Эти отношения удобно проверять как рёбра графа.

Минимальная проверка ниже ищет отсутствие самоссылки, обратной ссылки и недоступные цели. Она не проверяет корректность языковых кодов, одинаковый набор альтернатив или соответствие перевода исходному документу. x-default обрабатываем отдельно: правило обратной языковой ссылки для него в примере не применяем.

Связь

Что наблюдаем

Что из этого не следует

Редирект

Переход и конечный ответ в снимке

Что поисковик уже переобошёл адрес

Canonical

Объявленная цель

Что Google выбрал именно её

Hreflang

Связи языковых версий

Что переводы корректны по содержанию

HTTP 200

Успешный HTTP-ответ

Что документ проиндексирован

3. Контракт снимка важнее выбора краулера

Скрипт получает JSON-массив. Одна строка - один запрошенный URL и его непосредственный ответ, до автоматического следования редиректу. canonical содержит одну уже выбранную аннотацию; hreflang - словарь язык → URL. Относительные значения поддерживаются для удобства диагностики, но их допустимость в исходной разметке надо проверять до нормализации.

В промышленной выгрузке дополнительно храните время запроса, User-Agent, окружение, исходные заголовки, все найденные аннотации и версию парсера. Не сворачивайте несколько canonical в одно поле без отдельного сообщения о конфликте. Словарь hreflang тоже скрывает повторяющиеся языковые коды - их нужно выявлять на этапе извлечения.

Нормализация в примере намеренно консервативна: разрешаем относительный адрес и убираем фрагмент. Не удаляем query-параметры, не меняем регистр пути и не склеиваем варианты со слешем. Иначе нормализатор может скрыть как раз тот дефект, который ищет аудит.

Данные для примера синтетические: это не результаты обхода реального сайта.

[
  {"url":"https://example.com/old","status":301,"location":"/middle"},
  {"url":"https://example.com/middle","status":301,"location":"/ru/item"},
  {"url":"https://example.com/ru/item","status":200,"canonical":"/missing","hreflang":{"ru":"/ru/item","en":"/en/item"}},
  {"url":"https://example.com/en/item","status":200,"canonical":"/en/item","hreflang":{"en":"/en/item"}}
]

Сохраните массив в snapshot.json. В нём четыре URL: старый адрес проходит два редиректа, русская карточка указывает canonical на отсутствующую в снимке цель, английская версия не возвращает ссылку на русскую.

4. Проверяющий скрипт на Python

Сохраните код в audit_graph.py. Он работает локально, не отправляет HTTP-запросы и использует только стандартную библиотеку. Порядок результатов стабилен: одинаковый снимок даёт одинаковый JSON.

import json
import sys
from urllib.parse import urldefrag, urljoin

REDIRECTS = {301, 302, 303, 307, 308}

def normalize(base, value):
    return urldefrag(urljoin(base, value))[0]

def audit(rows):
    pages = {r['url']: r for r in rows}
    if len(pages) != len(rows):
        raise ValueError('Duplicate URL in snapshot')
    issues = set()
    def emit(rule, source, target):
        issues.add((rule, source, target))
    for source, page in pages.items():
        seen, current = set(), source
        while current in pages and pages[current]['status'] in REDIRECTS:
            if current in seen:
                emit('REDIRECT_LOOP', source, current)
                break
            seen.add(current)
            location = pages[current].get('location')
            if not location:
                emit('REDIRECT_NO_LOCATION', source, current)
                break
            current = normalize(current, location)
        else:
            if seen:
                if current not in pages:
                    emit('REDIRECT_TARGET_UNKNOWN', source, current)
                elif pages[current]['status'] != 200:
                    emit('REDIRECT_TARGET_NOT_200', source, current)
                if len(seen) > 1:
                    emit('REDIRECT_CHAIN', source, current)
        canonical = page.get('canonical')
        if canonical:
            target = normalize(source, canonical)
            if target not in pages:
                emit('CANONICAL_TARGET_UNKNOWN', source, target)
            elif pages[target]['status'] != 200:
                emit('CANONICAL_TARGET_NOT_200', source, target)
        alternates = page.get('hreflang', {})
        if alternates and source not in {
            normalize(source, value) for value in alternates.values()
        }:
            emit('HREFLANG_NO_SELF', source, source)
        for language, value in alternates.items():
            target = normalize(source, value)
            if target not in pages:
                emit('HREFLANG_TARGET_UNKNOWN', source, target)
                continue
            other = pages[target]
            if other['status'] != 200:
                emit('HREFLANG_TARGET_NOT_200', source, target)
            backlinks = {
                normalize(target, v)
                for v in other.get('hreflang', {}).values()
            }
            if language != 'x-default' and source not in backlinks:
                emit('HREFLANG_NO_RETURN', source, target)
    return [dict(zip(('rule', 'source', 'target'), row))
            for row in sorted(issues)]

if __name__ == '__main__':
    with open(sys.argv[1], encoding='utf-8') as handle:
        result = audit(json.load(handle))
    print(json.dumps(result, ensure_ascii=False, indent=2))
    sys.exit(1 if result else 0)

Запуск:

python3 audit_graph.py snapshot.json > issues.json

Код завершения 1 означает наличие диагностик, 0 - их отсутствие. Ошибка формата входа тоже завершает процесс с ошибкой, поэтому в CI нужно отдельно учитывать неуспешный запуск анализатора и валидный отчёт с нарушениями. Для полноценного сервиса стоит развести эти состояния разными кодами и добавить проверку схемы.

На приведённом снимке ожидаются три сообщения:

Правило

Источник

Причина

REDIRECT_CHAIN

/old

До конечной страницы два перехода

CANONICAL_TARGET_UNKNOWN

/ru/item

/missing отсутствует в снимке

HREFLANG_NO_RETURN

/ru/item

/en/item не ссылается обратно

UNKNOWN принципиально отличается от 404. Цели могло не оказаться в очереди обхода. Правильный следующий шаг - дозапросить её, а не сразу менять разметку. Для canonical на HTTP 404, присутствующего в снимке, скрипт вернёт уже CANONICAL_TARGET_NOT_200.

При цикле отчёт содержит диагностику для каждого исходного адреса, который в него попадает. В большом аудите их удобно объединять в одну задачу по компоненте графа. Здесь сохраняем отдельные источники, чтобы не терять адреса, требующие проверки.

5. Что этот инструмент пока пропускает

Это компактный валидатор снимка, а не замена краулеру. Он не рендерит JavaScript, не анализирует содержимое и не проверяет директивы индексации. Он также не различает намеренный внешний canonical и ошибочную ссылку на другой домен: оба случая требуют загрузки цели и контекста.

Отдельный слой должен проверять robots.txt, meta robots и X-Robots-Tag. Запрет обхода нельзя превращать в вывод «URL отсутствует в поиске»: robots.txt управляет обходом и не гарантирует исключение URL из выдачи.

Для сайтов с клиентской генерацией разметки храните HTTP-снимок и результат рендеринга отдельно. Иначе изменение DOM легко принять за изменение серверного шаблона. Для нестабильных ответов сохраняйте повторные наблюдения: одиночный 503 в момент релиза и устойчивый сбой - разные операционные ситуации.

Важное ограничение реализации: обход цепочки запускается для каждого URL. На длинных общих цепочках работа повторяется. Для больших выгрузок стоит кэшировать конечные узлы и использовать поиск компонент сильной связности для циклов. Начинать с такой оптимизации необязательно: сначала проверьте полноту данных и правильность правил.

6. Как встроить проверки в релиз

Не стоит блокировать деплой по числу всех предупреждений. Первый запуск почти наверняка обнаружит старые проблемы. Зафиксируйте исходный набор диагностик и отслеживайте новые нарушения по ключу (rule, source, target).

Предлагаемый регламент команды:

  1. Выбрать небольшой набор критичных URL по шаблонам и языкам: главная, категории, карточки, пагинация, старые адреса миграции.

  2. Снять ответы в тестовом окружении и проверить соответствие конфигурации ожидаемым публичным адресам. Не сравнивать staging и production простым текстовым diff без карты доменов.

  3. Запустить валидатор и сохранить снимок вместе с версией правил.

  4. Остановить релиз при новых циклах или недоступных целях у критичных шаблонов. Неизвестные цели сначала дозагрузить.

  5. После релиза повторить сбор на публичном сайте: CDN, правила edge-редиректов и серверная конфигурация могли отличаться.

Уровень

Условие

Действие команды

Блокировка

Новый цикл на критичном маршруте

Исправить до выпуска

Проверка

Новый canonical на ответ не 200

Подтвердить назначение и исправить конфликт

Дозагрузка

Цель не попала в снимок

Получить недостающие данные

Наблюдение

Лишний переход без поломки маршрута

Запланировать упрощение

Это пример политики проекта, а не универсальная шкала тяжести SEO-ошибок. Для сайта с несколькими страницами и крупного международного каталога пороги будут разными.

7. Где уместна LLM

Модель можно использовать после детерминированных проверок: сгруппировать сообщения по шаблонам, подготовить описание задачи или объяснить цепочку переходов. На вход ей достаточно проверенных фактов и идентификаторов правил.

Критерий закрытия задачи должен оставаться проверяемым: целевой URL отвечает ожидаемым кодом, цикл исчез, обратная языковая ссылка присутствует. Убедительное объяснение модели не заменяет повторный прогон на новом снимке.

Практический результат такого аудита - небольшое число воспроизводимых задач. У каждой есть исходный адрес, проблемная связь, наблюдение и условие исправления. Начните с одного шаблона и нескольких языковых версий: если команда может повторить ошибку и подтвердить исправление одним запуском, автоматизация уже выполняет свою работу.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.