Проблемы сканирования — реестр кодов
Каждая проблема сканирования Pro — стабильный код из единого реестра Rankbeam\Seo\Pro\Scanning\IssueRegistry. Сканеры не придумывают коды на месте: каждая проблема создаётся через IssueRegistry::make(), который берёт серьёзность и поле из реестра и отклоняет любой неопределённый код. Поэтому каталог ниже — контракт для ваших интеграций: панели, экспорт и оценка Pro читают коды, а не разбирают сообщения. Бесплатный seo:audit использует собственный реестр метаданных ядра с более узким охватом и некоторыми отличающимися кодами hreflang.
Каждый код содержит:
- id — стабильную строку, сохранённую как
seo_scan_issues.issue_type. - severity —
critical,warningилиnotice; фиксирована для кода (мы разделяем коды, а не меняем серьёзность одного). - field — соответствующее поле
seo_metaлибо page для проблем уровня страницы. - execution class — что требуется для обнаружения (ниже).
- evidence — ключи массива
contextпроблемы.
Классы выполнения
Каждая проверка относится ровно к одному из трёх классов по требованиям выполнения:
| Класс | Требует | Кто выполняет |
|---|---|---|
| metadata | Модель и резолвер ядра, без загрузки страницы | Сканирование модели (PageScanner); бесплатный seo:audit покрывает часть проверок метаданных |
| rendered | Отдаваемый HTML страницы (запрос к ядру приложения внутри процесса или внешняя загрузка) | Сканирование URL (UrlScanner) |
| network | Исходящий запрос для проверки отдельного адреса назначения (canonical на другой URL) | Сканирование URL, всегда через SsrfGuard |
Поэтому бесплатный аудит внутри процесса не может быть эквивалентен полному сканированию Pro: только коды metadata вычисляются без рендеринга страницы, и только конвейер Pro загружает отрендеренный HTML и проверяет канонические адреса по сети. Фильтруйте реестр по классу через IssueRegistry::byClass(IssueRegistry::EXEC_METADATA).
Коды метаданных
Обнаруживаются по модели и резолверу через PageScanner. (missing_title, missing_description и коды длины также выдаёт сканирование отрендеренного URL, измеряя отдаваемый <head>; коды и смысл одинаковы.)
| Код | Серьёзность | Поле | Данные | Значение |
|---|---|---|---|---|
missing_title | critical | title | — | Нет заголовка и вычисляемого резервного значения. |
missing_description | warning | description | — | Нет метаописания и вычисляемого резервного значения. |
missing_og_image | notice | og_image | — | Нет изображения Open Graph и вычисляемого резервного значения. |
missing_focus_keyword | notice | focus_keywords | — | Не задано фокусное ключевое слово. |
duplicate_title | warning | title | title, duplicate_urls | Заголовок повторяется на других страницах той же локали. |
duplicate_description | warning | description | description, duplicate_urls | Описание повторяется на других страницах той же локали. |
title_too_long | warning | title | length, max, script | Итоговый заголовок превышает рекомендацию для письменности (60 для латиницы, около 30 для CJK). |
title_too_short | notice | title | length, min, script | Итоговый заголовок короче нижней границы для письменности (30 для латиницы, около 15 для CJK). |
description_too_long | warning | description | length, max, script | Итоговое описание превышает рекомендацию для письменности (160 / около 80). |
description_too_short | notice | description | length, min, script | Итоговое описание короче нижней границы для письменности (70 / около 35). |
robots_conflict_indexing | critical | robots | robots | Директива robots содержит одновременно index и noindex. |
robots_conflict_following | warning | robots | robots | Директива robots содержит одновременно follow и nofollow. |
noindex_warning | warning | robots | robots, canonical, page_url, shipping_signal | Страница с canonical на себя имеет noindex: эвристика для проверки, а не доказательство необходимости индексации. Выдаётся при сканировании модели и отрендеренного URL. |
invalid_canonical | critical | canonical | canonical | Каноническое значение не является допустимым URL. |
cross_domain_canonical | warning | canonical | canonical, page_url | Канонический URL указывает на другой хост. |
shared_canonical | notice | canonical | canonical | Несколько страниц объявляют одинаковый канонический URL. |
insecure_canonical | warning | canonical | canonical | Канонический URL http:// на сайте https. |
hreflang_invalid_code | warning | alternates | invalid_codes | Альтернативная версия hreflang использует значение, которое не является x-default или допустимым языковым кодом BCP-47. |
hreflang_missing_self_reference | warning | alternates | locale, page_url | Альтернативные версии объявлены, но ни одна не ссылается на локаль самой страницы (hreflang на себя). |
hreflang_duplicate_code | warning | alternates | duplicate_codes | Один код hreflang соответствует нескольким URL (неоднозначная группа). |
hreflang_missing_x_default | notice | alternates | languages | В многоязычной группе hreflang нет резервной версии x-default. |
aeo_missing_author | notice | schema | — | У статьи в структурированных данных страницы нет сущности автора (авторство / происхождение явно не указано в разметке). |
aeo_article_missing_date | notice | schema | — | У статьи в структурированных данных нет даты публикации/изменения (временная история явно не указана в разметке). |
Пороги длины берутся из политики длины ядра с учётом письменности (Pro 2.33): 60/160 для латиницы, около 30/80 для CJK, в графемах, поэтому сканирование не противоречит счётчикам редактора. Нижние границы (30 для заголовка, 70 для описания на латинице, примерно вдвое меньше для CJK) — нижний порог недостаточной оптимизации в сканировании, а ключ контекста script называет применённую группу. Длина измеряется по итоговым заголовку/описанию — реально выводимым значениям, включая резервные и суффикс заголовка.
Коды hreflang_* проверяют объявленные альтернативные версии hreflang страницы из alternates резолвера: неверные/повторяющиеся коды, отсутствие ссылки на себя и x-default в многоязычной группе. Они выполняются только при объявленных альтернативных версиях. Межстраничную взаимность («обратные теги») эти проверки метаданных не проверяют; необязательная сетевая проверка ниже загружает другую страницу.
Коды aeo_* — сигналы готовности к ответам (AEO): представлено ли содержимое статьи в структурированных данных? Они читают итоговый граф JSON-LD и срабатывают только для объявленной разметки статей (Article, BlogPosting, NewsArticle, …) без сущности author (явное авторство / происхождение) или datePublished / dateModified (явная временная история). Страница без статьи никогда не отмечается. Проверки управляются seo-pro.scan.checks.aeo (включено по умолчанию) и соответствуют бесплатному seo:audit.
missing_focus_keyword зависит от настройки
Замечание об отсутствии фокусного ключевого слова появляется только при включённом процессе ключевых слов ядра (seo.keywords.enabled, по умолчанию false). При отключении сканирование не отмечает страницу за отсутствие ключевого слова. Бесплатная команда seo:audit и редактор Filament читают тот же флаг ядра, поэтому сканирование, аудит и напоминание редактора всегда согласованы: переключатель один.
Коды отрендеренной страницы
Обнаруживаются UrlScanner по отдаваемому HTML: для целей того же хоста — запросом к ядру приложения внутри процесса без исходящего трафика, для внешних — защищённой загрузкой.
| Код | Серьёзность | Поле | Данные | Значение |
|---|---|---|---|---|
http_error | critical | page | status | URL ответил статусом 4xx/5xx. |
empty_response | critical | page | — | URL вернул пустое тело. |
missing_canonical | notice | canonical | — | В отрендеренном head нет <link rel="canonical">. |
noindex_page | notice | robots | robots | Отрендеренная страница имеет noindex (информация). Для страницы noindex с canonical на себя вместо этого выдаётся учитываемый в оценке noindex_warning. |
missing_h1 | notice | page | — | Нет заголовка <h1>. |
multiple_h1 | notice | page | count | Более одного <h1> (информация). |
missing_image_alt | warning | page | count, total, sample | У изображений контента нет атрибута alt (явный alt="" означает декоративное изображение и не отмечается). |
thin_content | notice | page | word_count, threshold, segmenter | Текст страницы короче настроенного числа слов. Их считает токенизатор чек-листа: пробелы для письменностей с разделителями, словарная сегментация ICU (segmenter: intl, требует ext-intl) для китайского, японского и тайского — японская статья из 400 слов не считается одним «словом». |
mixed_content | warning | page | count, sample | Подресурсы http:// на странице https. |
html_lang_missing | notice | page | — | Нет <html lang> или он пуст. Вспомогательные технологии могут выбрать неподходящий голос. |
html_lang_invalid | notice | page | declared | Значение lang не является тегом BCP-47 (english, en_US с подчёркиванием, jp). |
html_lang_mismatch | warning | page | declared, declared_script, detected_script | Видимый текст написан письменностью, не соответствующей объявленному языку: lang="en" на японской странице, lang="ru" на латинице. Только уровень письменности (угадать неверный язык среди языков латиницы нельзя, и сканер не гадает); требуется ≥40 букв текста. |
Сетевые коды
Обнаруживаются UrlScanner только при включении соответствующего флага: seo-pro.scan.url_checks.check_canonical_target для канонической цели, check_hreflang_reciprocity для альтернативных версий hreflang. Каждая цель загружается через SsrfGuard (список разрешённых схем, область хостов, отклонение частных IP, бюджеты перенаправлений/времени/размера), без следования перенаправлениям, чтобы перенаправляющий канонический URL был виден. Canonical или альтернативная ссылка на себя пропускается: сама страница только что загружена.
| Код | Серьёзность | Поле | Данные | Значение |
|---|---|---|---|---|
blocked_url | notice | page | reason | Цель отклонена SsrfGuard до любого HTTP-запроса. |
canonical_target_broken | critical | canonical | canonical, status | Canonical указывает на страницу с HTTP-ошибкой. |
canonical_target_redirect | warning | canonical | canonical, status, location | Canonical указывает на перенаправляющую страницу; укажите конечный URL. |
canonical_target_noindex | warning | canonical | canonical | Canonical указывает на страницу, которая сама имеет noindex. |
canonical_target_blocked | notice | canonical | canonical, reason | Каноническую цель не удалось проверить (отказ защиты / недоступность). |
hreflang_not_reciprocal | warning | alternates | hreflang, href, status | Объявленная альтернативная версия не ссылается на исходную страницу. Пара hreflang может игнорироваться; само по себе это не запрещает индексацию перевода. |
hreflang_target_unverified | notice | alternates | hreflang, href, reason | Альтернативную версию не удалось загрузить (отказ защиты, ошибочный статус, перенаправление, превышение размера), поэтому взаимность не проверялась. Нет доказательств, а не найден дефект. |
Проверка взаимности загружает максимум hreflang_max_alternates (по умолчанию 10) целей на страницу, включая x-default, пропуская повторы и саму страницу. Коды метаданных hreflang_* уровня модели выше проверяют объявленный список; только эта проверка требует другую страницу.
Все сетевые пути используют общий SsrfGuard; модель угроз и остаточный риск TOCTOU описаны в SECURITY.md.
Как коды влияют на оценку
SEO-оценка Pro — 100 − сумма фиксированных штрафов за каждую учитываемую проблему с весом по серьёзности выше. Большинство кодов учитывается; некоторые намеренно исключены: missing_focus_keyword (рекомендация), noindex_page и multiple_h1 (информация), blocked_url / canonical_target_blocked / hreflang_target_unverified («не удалось проверить» ≠ дефект) и коды hreflang_*, html_lang_* и aeo_* (рекомендательные сигналы, пока исключённые из оценки). Полный список и штраф каждого кода приведены на странице оценки.
Жизненный цикл проблемы
Проблема — не просто строка, существующая до исправления. У неё есть жизненный цикл: сканирование согласует проблемы цели, а не стирает и создаёт заново. У каждой стабильная идентичность: цель (scannable_type + scannable_id для модели или url для маршрута/карты сайта) плюс issue_type. Каждый код выдаётся максимум один раз на цель за сканирование: коды с несколькими нарушителями (missing_image_alt, mixed_content, hreflang_*, …) объединяют случаи в одну строку с count / sample. Поэтому идентичность уникальна.
При каждом сканировании для каждой цели:
- находка без существующей строки создаётся как
openс отметкойdetected_at; - находка, совпавшая с открытой строкой, обновляет данные и сохраняет исходный
detected_at— стабильное первое обнаружение, которое больше не сбрасывается при каждом сканировании; - открытая проблема, которую завершённая проверка больше не находит, отмечается
fixedсresolved_at; строка сохраняется, а не удаляется, чтобы зафиксировать настоящее исправление; - проблема
fixed, которая вернулась, открывается повторно в той же строке (регрессия), обновляяdetected_at; - проблема, отмеченная пользователем как
ignoredв панели, не изменяется.
| Статус | Значение | Кто задаёт |
|---|---|---|
open | Присутствует сейчас. | Сканирование (новая или повторно найденная) |
fixed | Присутствовала, больше не находится. | Сканирование автоматически, при следующем запуске, который её не находит |
ignored | Отключена пользователем; исключена из открытых счётчиков и оценки. | Действие игнорирования в панели |
Поскольку исправления сохраняются, а не отбрасываются, отчёт под своим брендом может показывать настоящие количества исправленных/новых проблем за период вместо разницы снимков отчётов. Все потребители количества открытых проблем — панель, команда seo-pro:scan-status, оценка — фильтруют по open, поэтому сохранённые строки fixed не увеличивают показатели. Исправленные строки относятся к устранившему их запуску и удаляются по обычному сроку хранения запусков.
Конфигурация
// config/seo-pro.php → 'scan'
'url_checks' => [
'enabled' => true,
'crawl_external' => false, // fetch external URL targets (guarded)
'check_canonical_target' => false, // EXEC_NETWORK canonical validation (guarded)
'check_hreflang_reciprocity' => false, // EXEC_NETWORK hreflang link-back crawl (guarded)
'hreflang_max_alternates' => 10, // targets fetched per page by that crawl
],
'checks' => [
'length' => true, // title/description length (metadata + rendered)
'rendered_content' => true, // H1 / alt / thin content / mixed content / html lang
],
'content' => [
'min_word_count' => 200, // thin_content threshold
'evidence_sample' => 5, // max example URLs stored per issue
],Бюджет размера ответа защищённых запросов — seo-pro.http.max_response_bytes (по умолчанию 2 МБ); сканирование того же хоста внутри процесса не ограничено.
Совместимость (переименование кода)
Прежний единый код robots_conflict с двумя степенями серьёзности разделён, чтобы каждому коду соответствовала ровно одна:
| Старый код | Новый код | Серьёзность |
|---|---|---|
robots_conflict (index + noindex) | robots_conflict_indexing | critical |
robots_conflict (follow + nofollow) | robots_conflict_following | warning |
Если вы сохраняли robots_conflict или фильтровали по нему, перейдите на два новых кода.