Skip to content

Диагностика

English version

Начните отсюда:

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 по конвенции (isPublishedpublishedis_published, getStatusstatus). Когда имя аксессора не связано с колонкой, назовите поля явно: 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 для одной затронутой сущности. Эти три закрывают почти каждый вопрос мейнтейнера.