Тема
ТЗ — 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_runs | id, started_at, finished_at, pages_crawled, issues_found, status | журнал прогонов краулера |
cms_seo_engine_issues | id, run_id, type (broken_link|duplicate_meta|orphan|thin_content), url, details (json), resolved_at, severity | найденные проблемы, приоритизированные |
cms_seo_engine_dictionary | id, term, target_url, is_active | словарь терминов для auto-linking (whitelist) |
FK run_id — constrained() + 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_active | FormRequest, 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, SeoEngineIssueDetected | payload по таблице ниже |
cms/health | статус последнего прогона, отставание очереди | health-чек модуля |
Настройки (группа seo-engine)
| Ключ | Тип | Дефолт | affectsPageCache | Описание |
|---|---|---|---|---|
seo-engine.enabled | bool | true | нет | Включение планового аудита |
seo-engine.crawl_schedule | string | weekly | нет | Периодичность планового прогона |
seo-engine.thin_content_threshold | int | 300 | нет | Порог символов для флага «тонкий контент» |
seo-engine.auto_linking_enabled | bool | false | нет | Включение автоматической простановки ссылок |
seo-engine.max_concurrent_jobs | int | 5 | нет | Параллельность обхода очередями (rate-limit самого себя) |
seo-engine.max_pages_per_run | int | 50000 | нет | Лимит страниц за один прогон на крупных сайтах |
seo-engine.crawl_blackout_hours | json | [] | нет | Часы, в которые плановый обход не запускается (пиковая нагрузка сайта) |
seo-engine.auto_linking_kill_switch | bool | false | нет | Kill-switch: аварийная остановка auto-linking без выключения модуля (аудит продолжает работать) |
Лимиты и квоты: max_pages_per_run и max_concurrent_jobs — обязательный барьер против самостоятельной DDoS-атаки на собственный сайт краулером; достижение max_pages_per_run завершает прогон штатно со статусом partial, не ошибкой, с отчётом «обход не завершён — увеличьте лимит или дождитесь следующего прогона».
API
| Метод | Путь | Доступ | Назначение |
|---|---|---|---|
| GET | /api/v1/admin/seo-engine/runs | admin (seo-engine.view) | Список прогонов краулера (keyset-пагинация) |
| GET | /api/v1/admin/seo-engine/issues | admin (seo-engine.view) | Список найденных проблем с фильтрами (приоритет, тип) |
| POST | /api/v1/admin/seo-engine/crawl | admin (seo-engine.manage) | Постановка внепланового обхода в очередь |
| POST | /api/v1/admin/seo-engine/dictionary | admin (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/health | health-чек (декларативно из манифеста) | ядро → 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, который не должен визуально ломать текст (лишний пробел, двойная ссылка); скорость краулинга не создаёт заметных задержек ответа сайта обычным посетителям.
Крайние случаи и типовые баги
- Краулер кладёт собственный сайт — без rate-limit параллельные джобы создают пиковую нагрузку →
max_concurrent_jobsиcrawl_blackout_hoursобязательны, обход не запускается в заданные часы пик. - Огромный сайт (миллионы страниц) →
max_pages_per_runограничивает прогон, статусpartial, продолжение — со следующего планового окна, не блокирует навсегда. - Цепочки редиректов (A→B→C) обнаруживаются и репортятся отдельным типом проблемы, не молча схлопываются — решение цепочки (A→C) остаётся на стороне SEO-базы ядра (
Redirect-модель), seo-engine только диагностирует. - Битые ссылки на внешние домены — таймаут внешнего запроса ≤2 с, недоступность не блокирует остальной обход (проверка исходящих ссылок изолирована по джобе на URL).
- Suggests-модуль выключен (
cms/seo-filterотсутствует) → страницы фильтров не исключаются из orphan-детекта — могут ложно попасть в отчёт как orphan; ТЗ фиксирует это как ожидаемое ограничение при отсутствии модуля, не баг. - Модуль выключен посреди прогона → текущий
runзавершается со статусомcancelled, частичные issues сохраняются как исторические, следующий прогон при включении стартует заново (не продолжает с середины). - Гонка: два внеплановых обхода запущены одновременно (два админа нажали «обход сейчас») → второй запуск отклоняется 409 «обход уже выполняется», не создаёт параллельный
runповерх текущего. - Пустой сайт (0 страниц) → прогон завершается штатно,
pages_crawled=0,issues_found=0, без ошибок. - Auto-linking конфликтует с ручными правками контента — редактор одновременно правит ту же страницу вручную → auto-linking создаёт новую ревизию, а не правит текущий черновик напрямую; при конфликте редакторской правки и auto-linking — обычный
lock_version-конфликт ревизий ядра (409), не специфика модуля. - Дубли меты определяются некорректно на мультигороде/мультиязычии — одинаковый title у страниц разных городов/локалей не должен считаться дублем. ⚠️ Противоречие: наивная детекция дублей по строке title без учёта измерений
city_id/localeдаст ложные срабатывания. Разрешение: сравнение дублей — только внутри одного измерения (тот жеcity_id/locale), не глобально по всему сайту. - Противоречивые настройки:
auto_linking_enabled=trueпри пустом словаре → джоба отрабатывает штатно, проставляет 0 ссылок, не ошибка. - Конкурентное редактирование словаря двумя редакторами → уникальный индекс на
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запрещён