Диагностика¶
Начните отсюда:
bin/console indexnow:check # конфигурация, файлы ключей, движки, проводка
bin/console indexnow:explain 'App\Entity\Post' 42 # одна сущность, весь путь решения, ничего не отправляет
Вдвоём они закрывают большинство обращений без единой строки лога.
Не отправляется ничего¶
Идите по списку сверху вниз; каждый пункт называет улику, которая его подтверждает.
| Причина | Улика |
|---|---|
enabled: false |
skipped / disabled и одна info-строка на вызов |
dry_run: true |
skipped / dry_run, info-строка с полным телом запроса |
| невалидная конфигурация выключила IndexNow | одна critical-строка при загрузке, indexnow:check завершается ненулевым кодом |
| у сущности нет подходящего правила | indexnow:explain говорит, какое правило пропущено и почему |
| коллектор ни разу не сброшен | warning: N collected URL(s) discarded: the unit of work ended without flush() |
| хуки Doctrine не активны | indexnow:check печатает doctrine: entity hooks are NOT active |
| Messenger настроен, воркер не запущен | панель профайлера показывает отправленные в очередь URL; в логе воркера пусто |
Невалидная конфигурация — самая коварная. Плейсхолдер окружения нельзя проверить при компиляции, поэтому пустой
INDEXNOW_KEY в production не ломает контейнер — он логирует
indexnow: invalid configuration, IndexNow is disabled until it is fixed: <error> (run "bin/console indexnow:check")
на уровне critical и работает выключенным. Поставьте алерт на эту строку и включите indexnow:check в smoke-тесты
деплоя.
403 — файл ключа¶
Самая частая ошибка, и означает всегда одно: движок не смог проверить https://<host>/<key>.txt.
bin/console indexnow:check --host www.example.com
curl -i https://www.example.com/<key>.txt
Ответ должен быть 200, text/plain, тело — ровно ключ, без редиректа. Частые причины:
- маршруты бандла не импортированы (без Flex): добавьте
@IndexNowKitBundle/config/routes.phpвconfig/routes.yaml; key_file.enabled: false(или устаревшийserve_key_file: false) безkey_location;- CDN или reverse proxy отдаёт закэшированный старый файл после ротации ключа —
key_file.cache_max_ageпо умолчанию 300 секунд именно поэтому; - catch-all маршрут или firewall перехватывает
/{key}.txtраньше; - редиректы
http → httpsили наwwwна файле ключа; - запрос пришёл на другой хост, чем тот, которому принадлежит ключ, — контроллер отвечает 404 намеренно.
Пятый подряд 403 для хоста логируется один раз на critical с текстом «submissions for this host are not being
indexed». Это строка для пейджинга. Счётчик живёт в процессе: несколько воркеров считают каждый свои пять.
422 — unprocessable¶
URL не принадлежат хосту, под которым отправлены, или keyLocation невалиден. На практике:
- консольная команда или воркер сгенерировали URL на неверном хосте, потому что нет
base_urlдля хоста — см. multi-domain.md; - правило закрепило
host:, ключ которого не настроен; key_locationуказывает на другой хост. Слой конфигурации это отклоняет, так что такое бывает только с собственнымKeyProviderInterface.
indexnow:explain печатает хост и файл ключа для каждого URL, который отправил бы, — это решает вопрос сразу.
429 — rate limited¶
Движок попросил сбавить темп. Результат retryable, retryAfter заполнен, если движок его назвал.
- С Messenger транспорт повторяет и учитывает
Retry-Afterна Symfony 7.2+. Делать ничего не нужно. - С
dispatch: syncповторов нет: батч логируется и отбрасывается. Переходите на Messenger, если видите это регулярно. - Понизьте
throttle.max_requests_per_minute, если причина — ваш собственный трафик. Он считается на процесс. - Обычный триггер — миграция или импорт, объявляющий десятки тысяч URL разом; делайте это порциями из воркера.
Конкретный URL так и не отправляется¶
bin/console indexnow:explain 'App\Entity\Post' 42 --event=updated
Вывод идёт по правилам по порядку и печатает для каждого подписку на событие, результат when, фильтр fields и
разрешённые URL; затем для каждого URL — нормализованную форму, хост, замаскированный ключ, URL файла ключа и состояние
дебаунса. Типичные находки:
when: false— сущность не опубликована, страницы нет. Правильное поведение.- фильтр
fields— изменилось поле, которое правилу не важно. Добавьте его вfieldsили уберитеfields, чтобы отправлять при любом изменении. - не подписано —
eventsправила не включает это событие. debounced— отправлено в последниеdebounce.per_urlсекунд.--forceуindexnow:submitобходит это.no key for host— хост не вhostsи не хостbase_url.- ошибки правила — отсутствующий аксессор или маршрут, который не генерируется, печатаются красным и логируются на
error.
Публикация и снятие ведут себя странно¶
when вычисляется до и после изменения. true → false отправляет удаление, чтобы движки переобошли 404;
false → true — создание. Две вещи для проверки:
- Поле за
whenдолжно быть в change set.when: 'isPublished'сопоставляется полюpublishedпо конвенции (isPublished→published→is_published,getStatus→status). Когда имя аксессора не связано с колонкой, назовите поля явно:whenFields: ['status', 'visibleFrom']. - Удаление черновика ничего не отправляет — намеренно. Страница никогда не была публичной.
Панель профайлера¶
enabled, dry_run, dispatch и strict_hosts наверху говорят о режиме. Затем:
- Collected — URL, которые запрос буферизовал. Ноль здесь значит, что ни одно правило ничего не дало: проблема выше, в правилах или guard'ах.
- Results — что реально отправлено, по движку и хосту, с HTTP-кодом и
reason. Заполняется и при синхронной доставке, которая происходит после ответа. - С Messenger таблица результатов пуста намеренно, панель говорит, что HTTP-результаты в логе воркера.
- Key files — URL, который должен отдавать каждый управляемый хост, с замаскированным ключом. Откройте один в браузере как первую проверку.
- base_url — помечен, если не задан: тогда относительные URL и генерация в консоли или воркере не работают.
Логи¶
Всё идёт в Monolog-канал indexnow, каждое сообщение начинается с indexnow:.
# config/packages/dev/monolog.yaml
monolog:
handlers:
indexnow:
type: stream
path: '%kernel.logs_dir%/indexnow.log'
level: debug
channels: ['indexnow']
На время диагностики ставьте debug: причина, по которой правило решило не давать URL, логируется только на этом
уровне. Все тексты и уровни — в руководстве по эксплуатации.
Ключи замаскированы в каждой строке, включая тела ответов и сообщения исключений, — эти файлы безопасно прикладывать к багрепорту.
Стейджинг отправил свои URL¶
| Симптом | Причина | Исправление |
|---|---|---|
Bing/Яндекс показывают URL staging.example.com, или в логе failed / unprocessable (422) по ним |
стейджинг работает с боевым ключом и без dry_run; URL сгенерированы на его хосте |
вне production задайте INDEXNOW_DRY_RUN=1 (или INDEXNOW_ENABLED=0); с core 0.6 check на такой копии падает |
| стейджинг отдаёт боевой файл ключа | key_file.enabled включён везде |
key_file.enabled: false вне production — ни один движок не сможет проверить ключ на этом хосте |
| движки проиндексировали страницы стейджинга | хост стейджинга ответил на них 200 и отдал ключ |
отдавайте на стейджинге 410 (или noindex + запрет в robots.txt) и ротируйте ключ, если он утёк |
| preview-окружение должно отправлять нарочно | — | задайте там явный dry_run: false; check тогда предупреждает, а не падает |
Дубли с memory и несколькими воркерами¶
| Симптом | Причина | Исправление |
|---|---|---|
| один и тот же URL отправляет каждый воркер в течение минут | debounce.store: memory — на процесс; у каждого веб- и queue-воркера своё окно |
debounce.store = общий кэш; check предупреждает о memory |
| дубли сразу после сбоя кэша | store fails open: пока кэш лежит, дедупликации нет | ожидаемо и ограничено (один запрос на URL); следите за частотой warning debounce store unavailable |
| дубли после деплоя | общий кэш сброшен или изменился debounce.key_prefix |
безвредно один раз; держите префикс стабильным для приложения |
Дубли после сбоя кэша¶
Debounce store fails open. Если Redis или пул кэша недоступен, дедупликация пропускается и пишется warning
(debounce store unavailable, submitting without de-duplication). Видимый симптом — всплеск повторных отправок, а не
потерянные, и это правильный размен: дубль стоит один запрос, пропуск оставляет устаревший контент в индексе.
indexnow:check читает тестовый ключ через настроенный пул и печатает debounce: 600s per URL, shared through cache
pool "cache.app" (RedisAdapter) либо ошибку с именем пула, если он непригоден; debounce.store: memory помечается
как «на процесс».
indexnow:sitemap в read-only контейнере¶
SitemapReader держит каждый документ во временном файле во время разбора. При readOnlyRootFilesystem и без
записываемого /tmp дефолтный sitemap.spool: auto падает на память и пишет один warning на прогон; память ограничена
sitemap.max_bytes на документ (50 МиБ), так что ничего не ломается, просто стоит RAM. Смонтируйте emptyDir на
/tmp (или задайте TMPDIR / sitemap.spool_dir на записываемый том), чтобы вернуть временный файл, или задайте
spool: memory явно. indexnow:check печатает, куда spool'ятся документы и почему temp dir непригоден.
Sitemap, обрывающийся на середине («ends early (truncated download or broken sitemap)»), или потерянное соединение
повторяются sitemap.fetch_retries раз; если всё равно не вышло, прочитанные URL отправляются, команда завершается с 1,
и повторный запуск безопасен (IndexNow идемпотентен, debounce store поглощает повторы). Для ежедневного cron с
--changed-since берите окно шире интервала ("2 days"), чтобы упавший прогон не оставил дыры.
Как просить помощи¶
Приложите вывод bin/console indexnow:check, релевантные строки лога indexnow и вывод
bin/console indexnow:explain для одной затронутой сущности. Эти три закрывают почти каждый вопрос мейнтейнера.