Skip to content

ТЗ — SEO-движок (аудит+краулер) (cms/seo-engine)

Слой: 🟡 функц-модуль · Зрелость доноров: ★★★ · Донор: gulaev-dev (gulaev-dev/src/app/Domains/Seo/) Статус: ТЗ к разработке

Назначение и возможности

Внутренний технический аудит сайта: собственный краулер обходит сайт очередями и находит проблемы — битые ссылки, дублирующиеся мета, orphan-страницы, тонкий контент. Дополнительно умеет проставлять внутренние ссылки по словарю терминов. Регулярный прогон по расписанию, отчёты доступны в админке. Краулер обходит собственный сайт — это не внешний SEO-сервис, поэтому он обязан беречь собственную инфраструктуру.

  • Краулер по внутренним ссылкам сайта, обход через очереди (не блокирует основной поток)
  • Поиск битых ссылок (внутренних и исходящих) с кодом ответа
  • Поиск дублирующихся мета-тегов (title/description) между страницами
  • Обнаружение orphan-страниц (нет входящих внутренних ссылок)
  • Оценка «толщины» контента (длина текста ниже порога — кандидат на доработку)
  • Auto-linking: автоматическая простановка внутренних ссылок по словарю терминов (только whitelist)
  • Отчёты по прогонам в Filament с историей и диффом между прогонами
  • Плановый прогон через scheduler с настраиваемой периодичностью

Зависимости и выключение

requires: ядро · suggests: cms/seo-filter (учёт сгенерированных посадочных при обходе)

Поведение при выключении: аудит и auto-linking не выполняются, ранее найденные проблемы остаются в отчётах как исторические (не удаляются), сайт продолжает работать без диагностики — деградация наблюдаемости, не функциональности сайта. Внешних платных API модуль не требует (обход своего же сайта); исходящие ссылки проверяются с бюджетом таймаута, не влияют на публичный трафик.

Модель данных

ТаблицаКлючевые поляПримечание
cms_seo_engine_runsid, started_at, finished_at, pages_crawled, issues_found, statusжурнал прогонов краулера
cms_seo_engine_issuesid, run_id, type (broken_link|duplicate_meta|orphan|thin_content), url, details (json), resolved_at, severityнайденные проблемы, приоритизированные
cms_seo_engine_dictionaryid, term, target_url, is_activeсловарь терминов для auto-linking (whitelist)

FK run_idconstrained() + index(); type/severity — PHP Enum; details — JSONB; cms_seo_engine_runs — журнал, кандидат на BRIN по started_at.

ПДн-паспорт: модуль ПДн не хранит — предмет аудита это структура и контент собственного сайта (URL, мета, ссылки), не данные посетителей. Ретеншн: cms_seo_engine_runs хранит историю прогонов без TTL по умолчанию (полезно для диффа) — при росте БД рекомендуется настройка history_retention_days (см. «Настройки»).

Входные и выходные данные

Входы:

ИсточникПоляЧем валидируется
Внепланового обхода API/команда— (только авторизация)seo-engine.manage, идемпотентная постановка в очередь
Форма словаря auto-linking (Filament)term, target_url, is_activeFormRequest, target_url — только внутренние ЧПУ ядра (whitelist по route()/репозиторию страниц), внешние URL запрещены
Плановый триггер (ScheduleRegistrar)crawl_scheduleконфигурация настройки, без пользовательского ввода
Внутренний обход страниц (собственный HTTP-клиент джобы)HTML-ответ страницы сайтапарсер извлекает мета/ссылки/текст, не доверяет произвольным заголовкам как командам

Выходы:

ПотребительДанныеФормат
Filament adminсписок прогонов и проблем с диффомkeyset-JSON через API
cms/seo-filter (в обратную сторону: seo-engine потребляет его сервис)см. «События и обмен»
Контент ядра (ревизия страницы)проставленная auto-linking ссылкаобычная ревизия через ContentRepository/PageRepository, не raw SQL
Подписчики событийSeoEngineRunCompleted, SeoEngineIssueDetectedpayload по таблице ниже
cms/healthстатус последнего прогона, отставание очередиhealth-чек модуля

Настройки (группа seo-engine)

КлючТипДефолтaffectsPageCacheОписание
seo-engine.enabledbooltrueнетВключение планового аудита
seo-engine.crawl_schedulestringweeklyнетПериодичность планового прогона
seo-engine.thin_content_thresholdint300нетПорог символов для флага «тонкий контент»
seo-engine.auto_linking_enabledboolfalseнетВключение автоматической простановки ссылок
seo-engine.max_concurrent_jobsint5нетПараллельность обхода очередями (rate-limit самого себя)
seo-engine.max_pages_per_runint50000нетЛимит страниц за один прогон на крупных сайтах
seo-engine.crawl_blackout_hoursjson[]нетЧасы, в которые плановый обход не запускается (пиковая нагрузка сайта)
seo-engine.auto_linking_kill_switchboolfalseнетKill-switch: аварийная остановка auto-linking без выключения модуля (аудит продолжает работать)

Лимиты и квоты: max_pages_per_run и max_concurrent_jobs — обязательный барьер против самостоятельной DDoS-атаки на собственный сайт краулером; достижение max_pages_per_run завершает прогон штатно со статусом partial, не ошибкой, с отчётом «обход не завершён — увеличьте лимит или дождитесь следующего прогона».

API

МетодПутьДоступНазначение
GET/api/v1/admin/seo-engine/runsadmin (seo-engine.view)Список прогонов краулера (keyset-пагинация)
GET/api/v1/admin/seo-engine/issuesadmin (seo-engine.view)Список найденных проблем с фильтрами (приоритет, тип)
POST/api/v1/admin/seo-engine/crawladmin (seo-engine.manage)Постановка внепланового обхода в очередь
POST/api/v1/admin/seo-engine/dictionaryadmin (seo-engine.manage)Правка словаря auto-linking

Компоненты

Filament: дашборд прогонов с диффом между обходами, таблица проблем с фильтром по типу и приоритетом (severity: критично — битая ссылка на главной / средне — тонкий контент), редактор словаря auto-linking. Команды: cms:seo-engine:crawl --json, cms:seo-engine:report --json.

Демо-контент: SeoEngineDemoSeeder создаёт один завершённый демо-прогон с 2–3 проблемами разных типов (broken_link, duplicate_meta) и одним термином словаря — галерея /_gallery показывает отчёт без ручного обхода.

Фронтенд-бюджет: модуль не рендерит публичных блоков/виджетов — весь UI в Filament-админке, публичный фронтенд-бюджет не расходует.

Эксплуатация (ранбук): метрики seo_engine_pages_crawled_total, seo_engine_issues_found_total{type}, seo_engine_run_duration_seconds; алерт — прогон не завершился штатно (status = failed) или превысил ожидаемую длительность вдвое.

СимптомЧто проверить / команда
Краулер кладёт нагрузку на сайт в пиковые часыпроверить crawl_blackout_hours, max_concurrent_jobs
Прогон зависает / не завершаетсяcms:seo-engine:crawl --json с --dry-run, посмотреть max_pages_per_run и отставание очереди seo-engine
Auto-linking проставил лишнееauto_linking_kill_switch=true, откатить ревизии контента штатным механизмом ревизий ядра
Отчёт пустой при живом сайтепроверить seo-engine.enabled, права health-чека на обход (нет ли блокировки собственным rate-limit ядра)

Бэкап/рестор: cms_seo_engine_runs/issues/dictionary — в обычном бэкапе БД целиком. После рестора отчёты валидны как исторические данные; для актуального состояния сайта рекомендуется внеплановый cms:seo-engine:crawl сразу после рестора.

События и обмен

СобытиеКогдаPayload
SeoEngineRunCompletedзавершён обход сайтаrun_id, pages_crawled, issues_found
SeoEngineIssueDetectedнайдена новая проблемаrun_id, type, url

Слушателей нет, provides-контрактов не предоставляет; учитывает страницы cms/seo-filter через его публичный сервис (suggests).

Таблица взаимодействий:

Сущность/модульКаналНаправлениеЧто происходит
cms/seo-filter (suggests)сервис-вызов (4)seo-engine → seo-filterзапрос списка активных фильтр-URL, чтобы не считать их orphan/дублями
PageRepository/ContentRepository ядрасервис-вызов (4, requires)seo-engine → ядрочтение списка публикуемых страниц и записи их ревизий для auto-linking
cms/healthhealth-чек (декларативно из манифеста)ядро → seo-engineагрегирует статус последнего прогона и отставание очереди
очередь seo-engineочередь (5)seo-engine → воркеробход страниц джобами, ограниченный max_concurrent_jobs
Подписчики SeoEngineIssueDetectedсобытие (1)seo-engine → любойнапример cms/notifications-bus (если включён) шлёт алерт о критичной проблеме

Фоновая работа

Очередь seo-engine: краулинг джобами с ограничением max_concurrent_jobs, плановый запуск — через ScheduleRegistrar по crawl_schedule, минуя окна crawl_blackout_hours. Внешние HTTP-запросы (проверка исходящих ссылок) — только из очереди.

Производительность и кеш

Ожидаемые объёмы: от сотен до max_pages_per_run (деф. 50 000) страниц за прогон; крупные сайты обходятся частями за несколько плановых прогонов (status = partial). Горячий путь — сам обход не участвует в публичном page-cache; краулер обращается к собственным страницам как обычный посетитель (через кеш, если он есть) — это дополнительная защита от лишней нагрузки на БД.

Критичные индексы: (run_id, type) на cms_seo_engine_issues под фильтрацию отчёта, BRIN на cms_seo_engine_runs.started_at. Собственных тегов page-cache нет; auto-linking правит контент через обычный механизм ревизий — инвалидация стандартными событиями контента ядра (PageSaved/ContentEntrySaved), не собственным тегом.

Rate-limit самого себя: max_concurrent_jobs и crawl_blackout_hours — обязательный контроль, чтобы собственный краулер не создавал DDoS-подобную нагрузку на прод в часы пик; обход не должен быть быстрее, чем сайт способен обслуживать обычных посетителей.

Безопасность

Внеплановый обход и правка словаря — только под seo-engine.manage через FormRequest; auto-linking проставляет ссылки только на whitelist-термины словаря, произвольная простановка по свободному тексту запрещена. Права: seo-engine.view, seo-engine.manage.

Матрица ролей:

ДействиеАдминистраторМенеджерРедакторStudio
Просмотр отчётов (view)
Внеплановый обход, правка словаря (manage)
Включение auto-linking / kill-switch
Пометка проблемы «решена» (resolved_at)

UX-требования

Админ: пустое состояние отчётов — «Прогонов ещё не было» со ссылкой «Запустить обход сейчас»; массовое действие «пометить как решено» на списке проблем; человеческая ошибка при попытке добавить в словарь термин с внешним URL («Auto-linking поддерживает только внутренние ссылки»); подтверждение перед включением auto_linking_enabled (правит контент автоматически — предупреждение о характере изменений).

Посетитель: модуль не влияет на публичный UX напрямую — единственное касание — auto-linking, который не должен визуально ломать текст (лишний пробел, двойная ссылка); скорость краулинга не создаёт заметных задержек ответа сайта обычным посетителям.

Крайние случаи и типовые баги

  1. Краулер кладёт собственный сайт — без rate-limit параллельные джобы создают пиковую нагрузку → max_concurrent_jobs и crawl_blackout_hours обязательны, обход не запускается в заданные часы пик.
  2. Огромный сайт (миллионы страниц) → max_pages_per_run ограничивает прогон, статус partial, продолжение — со следующего планового окна, не блокирует навсегда.
  3. Цепочки редиректов (A→B→C) обнаруживаются и репортятся отдельным типом проблемы, не молча схлопываются — решение цепочки (A→C) остаётся на стороне SEO-базы ядра (Redirect-модель), seo-engine только диагностирует.
  4. Битые ссылки на внешние домены — таймаут внешнего запроса ≤2 с, недоступность не блокирует остальной обход (проверка исходящих ссылок изолирована по джобе на URL).
  5. Suggests-модуль выключен (cms/seo-filter отсутствует) → страницы фильтров не исключаются из orphan-детекта — могут ложно попасть в отчёт как orphan; ТЗ фиксирует это как ожидаемое ограничение при отсутствии модуля, не баг.
  6. Модуль выключен посреди прогона → текущий run завершается со статусом cancelled, частичные issues сохраняются как исторические, следующий прогон при включении стартует заново (не продолжает с середины).
  7. Гонка: два внеплановых обхода запущены одновременно (два админа нажали «обход сейчас») → второй запуск отклоняется 409 «обход уже выполняется», не создаёт параллельный run поверх текущего.
  8. Пустой сайт (0 страниц) → прогон завершается штатно, pages_crawled=0, issues_found=0, без ошибок.
  9. Auto-linking конфликтует с ручными правками контента — редактор одновременно правит ту же страницу вручную → auto-linking создаёт новую ревизию, а не правит текущий черновик напрямую; при конфликте редакторской правки и auto-linking — обычный lock_version-конфликт ревизий ядра (409), не специфика модуля.
  10. Дубли меты определяются некорректно на мультигороде/мультиязычии — одинаковый title у страниц разных городов/локалей не должен считаться дублем. ⚠️ Противоречие: наивная детекция дублей по строке title без учёта измерений city_id/locale даст ложные срабатывания. Разрешение: сравнение дублей — только внутри одного измерения (тот же city_id/locale), не глобально по всему сайту.
  11. Противоречивые настройки: auto_linking_enabled=true при пустом словаре → джоба отрабатывает штатно, проставляет 0 ссылок, не ошибка.
  12. Конкурентное редактирование словаря двумя редакторами → уникальный индекс на term предотвращает дубли; конфликт правки одного термина — 409 с человеческим сообщением через lock_version.

Донорский код

Что взятьПуть
Домен SEO-аудита, краулер, детекция проблемgulaev-dev/src/app/Domains/Seo/

Legacy-импорт: cms:seo-engine:import-legacy --source=<профиль> — перенос истории прогонов/проблем и словаря auto-linking с прежней инсталляции (gulaev-dev) в cms_seo_engine_* по ключу external_id; идемпотентен, --dry-run с отчётом расхождений. Актуально в основном для словаря auto-linking — история прогонов чаще не переносится (отчёты начинают копиться заново после переезда).

Тесты и приёмка

  • [ ] Контрактный тест: краулер на тестовом наборе страниц находит битую ссылку и дубль-мета
  • [ ] Плановый прогон выполняется через scheduler без блокировки основного потока
  • [ ] crawl_blackout_hours и max_concurrent_jobs не дают краулеру создать пиковую нагрузку
  • [ ] max_pages_per_run корректно завершает прогон статусом partial на большом сайте
  • [ ] Auto-linking применяет только термины из whitelist-словаря, не произвольный текст
  • [ ] auto_linking_kill_switch останавливает простановку ссылок без выключения аудита
  • [ ] Дубли меты определяются в рамках одного city_id/locale, не глобально
  • [ ] Orphan-страницы определяются корректно (нет входящих внутренних ссылок)
  • [ ] Повторный запуск обхода при выполняющемся прогоне отдаёт 409, не создаёт второй run
  • [ ] При выключении модуля исторические отчёты остаются доступны для чтения
  • [ ] Права seo-engine.view/seo-engine.manage разграничивают чтение отчётов и запуск/правку
  • [ ] Обход очередями не создаёт N+1 при массовой проверке ссылок
  • [ ] Контрактный набор cms-testing зелёный, пакет тестируется в testbench-изоляции
  • [ ] Feature-тест на каждый роут API; тестовая БД только seo-engine_test, migrate:fresh запрещён

Внутренняя база знаний студии