IndexNow для статического блога: что это, зачем и как я его прикручивал

Привет, %username%! Выкатил новый пост, поправил страницу, обновил title — и сидишь, ждёшь, пока поисковики соизволят переобойти сайт. День, два, неделя. А хочется, чтобы апдейт подхватился сразу после git push, без танцев с Search Console и ручных submission-форм.
Дальше — как закрыть эту боль одним шагом в GitHub Actions через мой CLI indexnow и почему оно вообще работает.
Что такое IndexNow и зачем он нужен#
Протокол IndexNow — это простой способ сказать поисковику: «эти URL изменились, переобойди их побыстрее, пожалуйста». Ты делаешь один HTTP-запрос с ключом и списком URL, поисковик в ответ подтверждает приём (или отбивает с осмысленной ошибкой) — и дальше уже его проблема, когда именно он переобойдёт страницы.
Главное: submission автоматически шарится между всеми участниками протокола. По спеке достаточно дёрнуть один эндпоинт (например, api.indexnow.org или bing.com/indexnow), и тот же запрос провалидируется и расшарится со всеми: Bing, Yandex, Naver, Seznam, Yep и другие участники. Google к протоколу формально не присоединился — для него по-прежнему остаётся sitemap + Search Console.
Аутентификация — самая «забавная» часть протокола. Ты придумываешь ключ (произвольная hex-строка длиной 8–128 символов), хостишь его в виде текстового файла на собственном домене, например https://example.com/<key>.txt, и присылаешь этот ключ в каждом запросе. Поисковик идёт по URL, читает файл, сверяет содержимое с тем ключом, что ты прислал. Совпало — submission принят. Нет — 403. Просто и без OAuth.
И тут же — мой любимый перл из документации Яндекс.Вебмастера
: «Ключ менять необязательно. Вы можете изменить ключ, если он стал известен третьим лицам.» То есть ключ, который ты по спеке обязан выложить публичным .txt на свой домен и который любой может вытащить одним curl, Яндекс предлагает ротировать, если он «стал известен третьим лицам». А он по дизайну протокола известен им всегда. Ну камон.
Зачем я написал свой CLI#
Дёргать API руками лениво, особенно когда у тебя на пуш приходится 10–50 изменившихся URL и хочется делать это из CI. Можно, конечно, нагуглить готовый пример с curl и jq, но:
- ошибки разбирать в bash больно;
- ретраи на 429/5xx писать руками лень;
- sitemap парсить из XML в bash — отдельный жанр перформанса;
- лимит протокола в 10 000 URL на запрос тоже надо учитывать.
Поэтому я написал на Go
маленький indexnow — сначала как CLI для разовых submission’ов, а с релиза v0.5.0 это уже и готовый GitHub Action. По дороге всё то, что обычно копится в самописных скриптах, я аккуратно унёс внутрь бинаря.
Заодно это стало хорошим pet-project’ом: тесты с -race, golangci-lint, mkdocs для документации, релизы через goreleaser, а теперь и composite Action с верификацией бинарей. Маленький инструмент, который и время экономит, и руки в форме держит.
Минимальный шаг в GitHub Actions#
Самый короткий путь — взять готовый Action и засунуть один шаг в workflow:
- uses: jtprogru/indexnow@v0
with:
key: ${{ secrets.INDEXNOW_KEY }}
sitemap: https://example.com/sitemap.xmlВсё. Этот шаг сам:
- определит ОС и архитектуру раннера (
linux/amd64,linux/arm64,darwin/arm64, …); - скачает релизный бинарь нужной сборки со страницы релизов;
- сверит его
sha256сchecksums.txt, который лежит рядом с бинарями в релизе — это защищает от «а что если кто-то подменил артефакт»; - закэширует бинарь в
runner.tool_cache, чтобы следующие раны не качали его повторно; - запустит
indexnow submitс переданными параметрами.
Ключ передаётся через env, а не флагом, — даже если в твоём шаге случайно отрабатывает set -x, ключ в логах не светится. В $GITHUB_STEP_SUMMARY после прогона падает короткий отчёт: сколько URL улетело, сколько батчей упало, куда отправлял. Не нужно лезть в полный лог раннера, чтобы понять, прошёл submit или нет.
Submit только тех URL, что изменились#
Слать весь sitemap при каждом пуше расточительно: поисковики переобойдут страницы и без тебя, IndexNow используешь именно для тех апдейтов, что важны прямо сейчас. Поэтому мой любимый рецепт — submit ровно того, что изменилось между предыдущим и текущим коммитом:
- uses: jtprogru/indexnow@v0
with:
key: ${{ secrets.INDEXNOW_KEY }}
sitemap: https://example.com/sitemap.xml
sitemap-since: ${{ github.event.before }}sitemap-since фильтрует записи sitemap по <lastmod>: всё, что старее переданного времени, отбрасывается.
Один граничный случай, который я заложил по умолчанию: записи без <lastmod> всегда проходят. «Нет сигнала о времени» трактуется как «могло измениться», и для IndexNow это безопасный дефолт — повторный submit идемпотентен, лишний раз дёрнуть не страшно. Куда хуже промолчать про реально изменившуюся страницу.
А если sitemap’у не доверяешь — urls-from#
sitemap-since хорош, когда <lastmod> у тебя действительно проставляется. Если нет (или ты не хочешь полагаться на свежесть sitemap’а вообще) — есть второй рецепт: собрать список URL из git diff между двумя коммитами и скормить его в Action через urls-from.
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: jtprogru/indexnow@v0
with:
key: ${{ secrets.INDEXNOW_KEY }}
urls-from: |
git diff --name-only "${{ github.event.before }}..HEAD" -- 'content/**/*.md' |
sed 's#^content/\(.*\)/index\.md$#https://jtprog.ru/\1/#'urls-from — это просто bash-сниппет, чей stdout (по одной строке на URL) превращается в источник URL. Сниппет крутится в $GITHUB_WORKSPACE, так что git видит весь checked-out репозиторий и git diff работает как обычно. Пустой вывод — это не ошибка, шаг тихо выйдет с submitted-count=0.
Вот тот самый маппинг: content/<slug>/index.md → https://jtprog.ru/<slug>/. Никакого парсинга sitemap, никаких лишних submission’ов на страницы, которые не трогал. fetch-depth: 0 обязательно — без полной истории git diff против github.event.before молча отдаст пустоту.
Тег @v0 и плавающие версии#
Одна из мелких, но важных деталей — модель версионирования.
@v0.7.0— конкретный релиз. Поведение зафиксировано, обновления только если ты сам поправишь workflow.@v0.7— все патчи внутри0.7.x, обновляется автоматически.@v0— все релизы внутри0.x, едет с каждым новым минором.
Плавающие теги (v0, v0.5) переставляются отдельным workflow на каждый релиз v0.Y.Z. То есть подписался один раз на @v0 — и едешь на последнем 0.x без правок workflow. Когда выкачу v1.0.0, тег @v0 останется на последнем релизе ветки 0.x: ты сам решаешь, переезжать ли на мажор. Никаких сюрпризов в духе «обновился — поведение сломалось».
Inputs, outputs и downstream-шаги#
Inputs Action’а зеркалят флаги CLI: источники URL (urls / file / sitemap / urls-from), sitemap-since, endpoint, host, key-location, fail-on, retry-knobs (max-retries, base-backoff, max-backoff), dry-run, verbose, quiet, user-agent. Между «попробовал локально, всё работает» и «засунул в CI» нет шага «а как там оно в Action называется» — один и тот же набор имён.
Outputs тоже из коробки:
exit-code— финальный код выхода CLI (0/1/2);submitted-count— сколько URL ушло (суммарно по батчам);failed-count— сколько батчей завершились не-2xx или ошибкой;report— одностраничная сводка; параллельно она же пишется в$GITHUB_STEP_SUMMARY.
Этими outputs удобно дёргать downstream-шаги: уронить алерт в Telegram, если failed-count > 0; или, наоборот, проигнорировать падение submit, если ты ему не доверяешь и не хочешь, чтобы из-за этого падал весь pipeline. Полный референс и готовые рецепты — в docs → GitHub Action
.
Если Actions не вариант#
GitLab CI, Drone, Woodpecker, cron на VPS, локальный запуск перед публикацией поста — для всех этих кейсов есть тот же бинарь, ставится двумя способами:
brew tap jtprogru/tap && brew install indexnow
# или
go install github.com/jtprogru/indexnow/cmd/indexnow@latestГотовые бинарники под Linux / macOS / FreeBSD в связках amd64/arm64 лежат на странице релизов
. И дальше тот же submit, только из шелла:
export INDEXNOW_KEY=8f7e6d5c4b3a29180706050403020100
indexnow submit \
--sitemap https://example.com/sitemap.xml \
--sitemap-since 2026-06-01T00:00:00ZИсточник URL — на выбор:
- позиционные аргументы:
indexnow submit https://a https://b; --file urls.txt(по одному URL на строку,#— комментарий);--stdin— типичный pipe из другого CLI (sitemap-to-urls | indexnow submit --stdin);--sitemap— URL или локальный путь к XML.
С sitemap’ом отдельная история: <sitemapindex> раскрывается рекурсивно (ходишь по всем дочерним), а .gz-варианты гунзипятся прозрачно — отдельный шаг с gunzip не нужен. На больших проектах с десятками сегментированных sitemap’ов это здорово упрощает жизнь.
Что CLI делает за тебя#
Если коротко — всё то, что обычно копится в скриптах вокруг такой задачи:
- Ретраи 429/5xx и transport-ошибок с экспоненциальным backoff’ом, jitter’ом и уважением к заголовку
Retry-After(понимает и секунды, и HTTP-date). Ретраи без backoff превращают деградацию в катастрофу — особенно если поисковик уже под нагрузкой. - Резка батчей по лимиту протокола (10 000 URL на запрос). Не нужно думать, что делать, когда у тебя 25 000 урлов: CLI сам режет на три батча, по каждому идёт отдельный submit и отдельный результат в отчёте.
- Несколько эндпоинтов сразу:
--endpoint bing,yandex,naver. Если ты по какой-то причине не доверяешь «один эндпоинт расшарит всем», передай список — CLI отправит в каждый и склеит результат. - Внятные exit code (
0— всё ок,1— submission failed по сети/HTTP/fail-on,2— usage error: кривые флаги, нет источника URL, нет ключа) и флаг--fail-on any|4xx|5xx|never. Например, на 429 ты можешь не хотеть падать (это не твоя проблема), а на 4xx по ключу — обязательно. - Управление ключом через
indexnow key:key gen --write public/генерит ключ и кладёт hosted-файл в нужную директорию,key verify --host example.com --key $KEYдёргает GET по hosted-файлу и сверяет содержимое.
key verify стоит гонять до первого submit — чтобы убедиться, что key-файл реально отдаётся не только тебе из браузера, но и поисковикам. Иначе все submit’ы будут возвращать 403, а ты будешь чесать репу и читать changelog в поисках сломанного релиза.
Тонкие места, на которых легко споткнуться#
fetch-depth: 0вactions/checkout. Без полной историиgit diffпротивgithub.event.beforeмолча отдаст пустоту: shallow-клон не видит предыдущий коммит. Симптом —submitted-count=0на каждом пуше, и ты долго гадаешь, почему.- Первый пуш в ветке.
github.event.beforeтам — нули (0000000...),git diff 0000000..HEADсваливается сfatal: bad revision. Прикрой fallback’ом наHEAD~1. - CDN кеширует hosted key-файл. Cloudflare, Fastly и прочие радостно складывают
.txtв кеш на пять-десять минут. Поменял ключ — поисковик в это окно ловит старую версию и отбивает 403 на твой свежий submit. После ротации — явный purge, без вариантов. <lastmod>без timezone.sitemap-sinceждёт RFC3339. Если Hugo (или чем ты там собираешь sitemap) пишет2026-06-02T14:00:00безZи без смещения, фильтр поедет на таймзоне раннера: на GitHub-hosted это UTC, у себя на сервере — что настроил. Один и тот же sitemap начнёт давать разные срезы в зависимости от того, где крутится workflow.- Параллельные эндпоинты ≠ страховка от лимитов. Отправил в
bing,yandex,naver— формально получишь три ответа, но Bing всё равно поделится submission’ом с остальными. Если поисковик отбил 429, лучше уменьшить частоту запусков, чем долбить параллельно во все доступные API.
Где взять и куда нести issue#
- Код, issues и звёздочки: github.com/jtprogru/indexnow
- Документация (EN + RU): jtprogru.github.io/indexnow
- Гайд по GitHub Action: jtprogru.github.io/indexnow/guides/github-action/
Если интегрируешь — поделись опытом в issues или прямо в комментариях. Особенно интересны истории про GitLab/Drone: под GitHub Action у меня самые подробные тесты, а на других CI оно проверяется реже.
PS: расскажи, как у тебя сейчас устроена индексация — Search Console + ручной запрос, sitemap-only, или что-то более хитрое? И насколько вообще IndexNow дал прирост по скорости индексации? Хочется собрать чуть более широкие данные, чем у меня одного.
Если у тебя есть вопросы, комментарии и/или замечания – заходи в чат , а так же подписывайся на канал .
О способах отблагодарить автора можно почитать на странице “Донаты ”.