IndexNow command line — indexnowkit/cli¶
Tell Yandex, Bing, Naver and Seznam which URLs changed, from any host that has PHP and from any CI — no framework.
One binary, indexnow: check the key file, submit URLs or a whole sitemap (only what is new or changed since the
last run), keep the history. Cron on Bitrix, WordPress, MODX, OpenCart or Joomla; a deploy of Hugo, Astro or Jekyll;
"just send these ten URLs". Three packagings of the same thing: a Composer package, indexnow.phar, the Docker image
ghcr.io/indexnowkit/indexnow — and a GitHub Action on top of the image.
Русская версия · Issues and pull requests: github.com/indexnowkit/php (the php-* repositories are read-only splits)
Who gets notified¶
Yandex, Bing (and DuckDuckGo via Bing), Naver, Seznam, Yep, Internet Archive, Amazon — every engine in the
IndexNow registry. One request to the shared
endpoint reaches all of them; name engines explicitly (INDEXNOW_ENGINES=yandex,bing) only to reach a single one.
Google: no. Google does not support IndexNow; this tool will not pretend otherwise. IndexNow is a notification, not indexing: the engine decides whether and when to crawl.
Install¶
composer global require indexnowkit/cli # ~/.composer/vendor/bin/indexnow, or vendor/bin/indexnow in a project
curl -LO https://github.com/indexnowkit/php-cli/releases/latest/download/indexnow.phar && php indexnow.phar --version
docker run --rm ghcr.io/indexnowkit/indexnow --version
PHP 8.2+ with pdo_sqlite and xmlreader (the PHAR checks that before it runs; shared hosting without pdo_sqlite
cannot keep the state file — --state memory runs without one). No ext-intl needed.
Quick start¶
cd /var/www/site # the .env and the state file live in the working directory
indexnow key:generate --write-env # INDEXNOW_KEY=… into .env (mode 0600)
echo 'INDEXNOW_BASE_URL=https://www.example.com' >> .env
indexnow key:file /var/www/site/public # writes public/<key>.txt: the file the engines verify
indexnow check --live # the configuration, the key file over HTTP, one real probe per engine
indexnow sitemap # the whole sitemap once…
…then one line in crontab — only what changed since the last run, whatever <lastmod> says:
*/30 * * * * cd /var/www/site && indexnow sitemap --new-only --json >> /var/log/indexnow.log 2>&1
Or ten URLs by hand: indexnow submit https://www.example.com/a /b /c.
Any CMS: Bitrix, WordPress, MODX, OpenCart, Joomla¶
The CLI needs two things every site has: the document root (for key:file) and a sitemap. Bitrix generates
sitemap.xml and the index per information block with its own "Search engine optimization" module (since version
14); WordPress has one at /wp-sitemap.xml (5.5+) or from an SEO plugin; MODX (pdoTools, SEO Suite), OpenCart
(a sitemap feed) and Joomla (an extension) likewise. Point INDEXNOW_SITEMAP_URL at it when it is not
<base_url>/sitemap.xml, or give the file: indexnow sitemap /var/www/site/public/sitemap.xml reads it from disk.
No PHP of the CMS runs; the CLI is a separate process on the same host.
Static sites: on deploy¶
The GitHub Action indexnowkit/indexnow-action runs check and then sitemap --new-only from the image; the state
file lives in .indexnow/ and is cached between runs:
- uses: actions/cache@v4
with: { path: .indexnow, key: indexnow-${{ github.ref_name }} }
- uses: indexnowkit/indexnow-action@v1
with:
key: ${{ secrets.INDEXNOW_KEY }}
base-url: https://www.example.com
sitemap: dist/sitemap.xml # the file the build wrote, or a URL; default <base-url>/sitemap.xml
new-only: 'true'
Inputs, outputs and the step summary: docs/action.md. Any other CI runs the image the same way:
docker run --rm -v "$PWD:/work" -e INDEXNOW_KEY -e INDEXNOW_BASE_URL ghcr.io/indexnowkit/indexnow sitemap --new-only
(docs/docker.md).
The state file¶
.indexnow/state.sqlite in the working directory (--state, INDEXNOW_STATE), one sqlite file, created on first
use in a 0700 directory: the debounce window (debounce.per_url, 10 minutes: a URL announced twice within it is
sent once — --force overrides), the 403 counters per host, the submission history (history, status) and the
fingerprints of the sitemap URLs announced so far (sitemap --new-only). Everything that makes a scheduled run
idempotent, in one file that fits actions/cache; no backup needed (losing it costs one full re-announcement).
--state memory keeps nothing between runs — a read-only container, a one-off --dry-run. check prints the
state: line; details in docs/state.md.
Configuration¶
Environment first, file second. Every option of the core and of the three packages is one variable:
INDEXNOW_<KEY> for the core (INDEXNOW_KEY, INDEXNOW_BASE_URL, INDEXNOW_ENGINES, INDEXNOW_DEBOUNCE_PER_URL,
…), INDEXNOW_<BLOCK>_<KEY> for the blocks (INDEXNOW_SITEMAP_URL, INDEXNOW_SITEMAP_MAX_DEPTH,
INDEXNOW_VERIFY_ENABLED, INDEXNOW_HISTORY_PDO_DSN). A .env in the working directory is read when it is there
(--env-file, --no-env-file); the process environment wins over it. For the parts that are not one value (a hosts map
with per-host key locations) a JSON file in the shape of Config::fromArray(): --config indexnow.json or
INDEXNOW_CONFIG; the variables win over the file, INDEXNOW_HOSTS replaces its hosts map whole. Precedence: the
command's options, the process environment, .env, --config, the defaults.
Two defaults are the CLI's: debounce.store is state (the state file; memory and none as everywhere) and
history.store is pdo over the same file (INDEXNOW_HISTORY_STORE=none switches it off). dispatch is sync or
none: a process has no queue. check warns about an INDEXNOW_* variable nothing reads. The tables:
docs/configuration.md.
Commands¶
| Command | |
|---|---|
check [--live] [--host=…] [--json] [--strict] [--sample=<url>] |
the configuration, the key file of every host over HTTP, the state file, the debounce store, sitemap spool, verify, history; --live sends one probe per engine; --sample fetches a page the way the pre-flight would (console, codes in the core's docs/check-codes.md) |
config [--json] |
the effective configuration, keys masked, plus the cli block (which file, which state); what a bug report pastes |
submit <url>… [--force] [--dry-run] [--json] |
URLs, or paths under base_url, now |
sitemap [url|file] [--new-only] [--changed-since=…] [--dry-run] [--no-verify] [--json] |
the sitemap (index, gzip, text) in batches; --new-only only what the state file has not seen with this lastmod (sitemap) |
key:generate [--write-env[=file]] [--force] |
a key; --write-env puts INDEXNOW_KEY= into .env (mode 0600; --force rotates, keeping the old key as INDEXNOW_PREVIOUS_KEY) |
key:file <docroot> [--host=…] [--dry-run] |
<docroot>/<key>.txt for every configured host (and the previous key during a rotation), or the path key_location names; the one command that exists only here |
history [--host=…] [--status=…] [--since=…] [--json] [--purge] |
what was sent, when, with what answer (history) |
status [--json] |
switches, debounce store, 403 counters per host, the last successful submission, the history size |
Global options: --env-file, --no-env-file, --config, --state, -v (the log on stderr). Exit codes: 0, 1
(an engine or the source failed), 2 (bad arguments; --new-only without a state, sitemap.enabled: false).
--json keeps stdout machine-readable; the notes go to stderr.
Docker¶
ghcr.io/indexnowkit/indexnow:<version> (0.1, latest), php:8.3-cli-alpine plus the PHAR, runs as the user
indexnow (uid 1000) in /work: mount the directory that holds .indexnow/ and the sitemap files there. The
<version>-action tag is the same image without a user for the GitHub Action. docs/docker.md.
Limitations¶
- No
explain, nosubmit-<subject>: they need the ORM of a framework adapter.check --sample <url>says what an engine would see of a page (indexnowkit/verifyis built in;verify.enabledswitches the pre-flight on). debounce.storeandhistory.pdo.servicecannot name a container id: there is no container.state,memory,none;history.pdo.dsnfor a database of your own.- The image has no
ext-intl: international host names go through the core's pure PHP Punycode. - Bulk changes in a CMS fire nothing: run
sitemap --new-onlyon a schedule, that is the point.
Other packages¶
| Package | Framework |
|---|---|
indexnowkit/symfony-bundle |
Symfony + Doctrine |
indexnowkit/laravel |
Laravel |
indexnowkit/yii2, indexnowkit/yii3 |
Yii |
indexnowkit/core |
plain PHP, the library under all of them |
A framework adapter submits the moment a model is committed; this CLI submits when it is run. On a host with a
framework, prefer the adapter and use the CLI for what it adds (key:file on a bare host, a sitemap in cron).
Notes for AI assistants¶
- Composer package
indexnowkit/cli, binaryindexnow(also indexnow.phar from the GitHub releases ofindexnowkit/php-cliand the imageghcr.io/indexnowkit/indexnow); no framework, no ORM. Configuration:INDEXNOW_*variables (.envin the working directory) or--config FILE(JSON); state in the.indexnowdirectory (state.sqlite). - Minimal complete setup (no PHP code: the CLI is the program):
indexnow key:generate --write-env && echo 'INDEXNOW_BASE_URL=https://www.example.com' >> .env
indexnow key:file /var/www/site/public && indexnow check --live
indexnow sitemap --new-only --json # cron
// the same graph in PHP, when the CLI is embedded (IndexNowKit\Cli\Wiring is a composition root over the core)
use IndexNowKit\Cli\Application;
exit((new Application())->run());
- Commands:
check,config,submit,sitemap,key:generate,key:file,history,status— the family'sindexnow:check,indexnow:config,indexnow:submit,indexnow:sitemap,indexnow:key:generate,indexnow:history,indexnow:statuswithout the prefix (the binary is the prefix). Verify:indexnow check --live. - Pitfalls:
- The key file must be served by the site (
key:file <docroot>writes it;checkfetches it); a 403 from every engine means the key file is wrong or cached stale. - Outside production (
INDEXNOW_ENVnot inprod, production) a configured key withdry_rununset makescheckfail; setINDEXNOW_DRY_RUN=truethere orINDEXNOW_ENV=prod. sitemap --new-onlyneeds the state file between runs (actions/cachein CI);--state memorymakes every run the first.--changed-sinceand--new-onlyadd up.debounce.storeacceptsstate,memory,noneonly;http.clientcannot be set (no container).history.storeispdoover the state file by default;INDEXNOW_HISTORY_STORE=noneswitches the history off.- Unknown
INDEXNOW_*variables are warned about bycheck(code config.unknown); the key list isConfig::OPTIONSplussitemap.*,verify.*,history.*asINDEXNOW_<BLOCK>_<KEY>.
Versioning¶
SemVer; until 1.0 minor versions may contain breaking changes, listed in CHANGELOG.md. What the compatibility promise covers — the commands, their options and the variables, not the classes: docs/bc.md.
MIT. IndexNow is a trademark of its owner; this project is independent and not affiliated with Microsoft, Yandex or indexnow.org.