Skip to content

IndexNow submission history — indexnowkit/history

What was announced to Yandex, Bing and the other IndexNow engines, when, and with what answer: the two shipped implementations of the core's Submission\SubmissionStoreInterface — a PSR-16 ring buffer for development and small sites, a database table (PDO) for production — plus the history and status commands of every adapter and the "recent submissions" list of the Symfony profiler panel. IndexNow is a notification, not indexing: the history tells you what your side did; whether a page was crawled is in Bing Webmaster Tools and Yandex.Webmaster.

Nothing is recorded until a store is named (history.store: null by default).

Packagist Downloads CI Coverage PHPStan PHP License

Русская версия · Issues and pull requests: github.com/indexnowkit/php (the php-* repositories are read-only splits)

Install

composer require indexnowkit/history        # brings indexnowkit/core
# Symfony: config/packages/indexnowkit.yaml       # Laravel: config/indexnow.php 'history' => [...]
indexnowkit:                                      # Yii2: 'history' => [...] of the component
    history:
        store: pdo               # null | psr16 | pdo
        pdo:
            service: ~           # the adapter's connection (default connection when null), or dsn: 'mysql:…'
            table: indexnow_submissions
        retention_days: 90

With store: pdo create the table first: docs/migrations.md has a Doctrine Migrations, a Laravel and a Yii2 snippet, each one line over History\Pdo\Schema::sql($driver). check prints history: 1 240 records, last 3 min ago (or the exception with the migration hint when the table is missing).

What is recorded

One record per Result — one batch of URLs at one engine — with the URLs after the core's normalization (tracking parameters already stripped), the engine, host, status (ok, pending, failed, skipped), the Reason, the HTTP code, and Result::$error cut to 1000 characters. Skipped results (debounced, dry-run, the pre-flight of indexnowkit/verify) are records too. Never a response body, a header, or the key. Personal data enters the history only when a URL carries it.

Store Where For
psr16 the adapter's cache behind debounce.store, a ring buffer of history.limit (500) records under <debounce.key_prefix>history.<n> one process, development, small sites: two workers recording at once may lose a record, an eviction empties the history
pdo the table history.pdo.table (one row per URL, the rows of one Result share a batch), indexes on url, at, (host, at) production; history --purge removes what is older than history.retention_days

Any other SubmissionStoreInterface you bind works with the commands too (recent() is enough); counting, "last submission" and --purge need History\HistoryStoreInterface (count(), last(), purge()), which both shipped stores implement.

Commands

bin/console indexnow:history --host=www.example.com --status=failed --since=2h --limit=20
php artisan indexnow:history --url=https://www.example.com/post-1        # exact URL after normalization
php yii indexnow/history --json                                           # {"records":[{at,status,reason,engine,http_status,error,urls[]}]}
bin/console indexnow:history --purge                                       # older than retention_days; --purge=30 for 30 days; one line for cron
bin/console indexnow:status --json                                         # enabled, dry_run, environment, dispatch, debounce store, 403 counters per host, last successful submission, history size, core version

indexnow:status is read-only: nothing is fetched, nothing is sent. Its JSON follows docs/status.schema.json.

Plain PHP

use IndexNowKit\Config;
use IndexNowKit\History\Pdo\PdoSubmissionStore;
use IndexNowKit\IndexNowKit;

$store = new PdoSubmissionStore(new PDO('sqlite:var/indexnow.sqlite'));
$store->createTable();                                            // or the migration of docs/migrations.md
$indexNow = IndexNowKit::create(Config::fromEnv(), submissionStore: $store);
$indexNow->submit(['https://www.example.com/post-1']);
foreach ($store->recent(10) as $record) { echo $record->at->format(DATE_ATOM), ' ', $record->result->status->value, ' ', implode(' ', $record->urls), PHP_EOL; }

Requirements

PHP 8.2+, indexnowkit/core ^0.10; ext-pdo with the driver of your database for store: pdo (sqlite, mysql, pgsql schemas shipped); indexnowkit/console ^0.3 for the commands (every adapter has it).

Notes for AI assistants

  • Composer package indexnowkit/history (add-on of indexnowkit/core). With a framework adapter nothing is wired by hand: history.store: pdo (or psr16) in the adapter's configuration binds the store; history.pdo.table needs the migration of docs/migrations.md first.
  • Minimal complete snippet (every use included):
use IndexNowKit\Config;
use IndexNowKit\History\Pdo\PdoSubmissionStore;
use IndexNowKit\IndexNowKit;

$store = new PdoSubmissionStore(new PDO('sqlite:var/indexnow.sqlite'));
$store->createTable();
$indexNow = IndexNowKit::create(Config::fromEnv(), submissionStore: $store);
$indexNow->submit(['https://www.example.com/post-1']);
$last = $store->lastFor('https://www.example.com/post-1');   // ?SubmissionRecord: urls, result (status, reason, httpCode, error), at
  • Verify: bin/console indexnow:history, php artisan indexnow:history, php yii indexnow/history list the records; indexnow:status / indexnow/status print the wiring and the last successful submission; indexnow:check / indexnow/check print the history: line.
  • Pitfalls:
  • history.store is null by default: installing records nothing. psr16 loses records when the cache is evicted or two workers write at once; production uses pdo.
  • --purge and the record counts need the shipped stores (History\HistoryStoreInterface); a custom SubmissionStoreInterface still lists with indexnow:history.
  • --url matches the URL after normalization (normalizer.strip_tracking_params is on by default): pass the URL without utm_*.
  • history.key_prefix and the PDO table name must not carry {}()/\@: (PSR-6) or anything but [A-Za-z0-9_] (SQL identifier).
  • dispatch: auto exists in Symfony and Yii2, not in Laravel; locales are router.locales (Laravel), router.languages (Yii2), framework.enabled_locales (Symfony).

Versioning

SemVer; until 1.0 minor versions may contain breaking changes, listed in CHANGELOG.md. What the compatibility promise covers: docs/bc.md.

MIT. IndexNow is a trademark of its owner; this project is independent and not affiliated with Microsoft, Yandex or indexnow.org.