koh / sylius-indexnow
IndexNow for Sylius: announces changed, launched and removed products and taxons per channel host. Derived per-host keys, production only, a retrying queue.
Package info
github.com/ohartwig/sylius-indexnow
Type:sylius-bundle
pkg:composer/koh/sylius-indexnow
Requires
- php: ^8.5.11
- doctrine/dbal: ^3.8 || ^4.0
- doctrine/migrations: ^3.9.8
- doctrine/orm: ^2.18 || ^3.0
- doctrine/persistence: ^3.3 || ^4.0
- guzzlehttp/psr7: ^2.13.1
- koh/indexnow: ^1.0
- psr/http-message: ^1.1 || ^2.0
- psr/log: ^2.0 || ^3.0
- sylius/sylius: ^2.2.9 || ^2.3.0
- symfony/config: ^7.4 || ^8.0
- symfony/console: ^7.4 || ^8.0
- symfony/dependency-injection: ^7.4 || ^8.0
- symfony/http-client: ^7.4 || ^8.0
- symfony/http-client-contracts: ^3.7.3
- symfony/http-foundation: ^7.4 || ^8.0
- symfony/http-kernel: ^7.4 || ^8.0
- symfony/lock: ^7.4 || ^8.0
- symfony/routing: ^7.4 || ^8.0
Requires (Dev)
- infection/infection: ^0.35.6
- phpstan/phpstan: ^2.3.1
- phpunit/phpunit: ^11.5 || ^12.0 || ^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
IndexNow for Sylius
Tells search engines within minutes which products, taxons and homepages of a Sylius shop were added, changed or removed — through IndexNow, which Bing, Yandex, Seznam, Naver, Yep and Amazon share among themselves. Bing's index is what ChatGPT search and Copilot read, so a new product can be found there the same day.
For Sylius 2. Built on the framework-agnostic koh/indexnow.
Install
composer require koh/sylius-indexnow
// config/bundles.php Koh\IndexNow\Sylius\KohIndexNowSyliusBundle::class => ['all' => true],
# config/routes/koh_indexnow.yaml — the key file, at the origin root koh_indexnow: resource: '@KohIndexNowSyliusBundle/config/routes.php'
bin/console doctrine:migrations:migrate # two small tables, shipped with the bundle
Then run the worker every few minutes, from cron or a Kubernetes CronJob:
bin/console koh:indexnow:run
That is the whole setup. Every enabled channel announces on its hostname:
- The key is derived per host from
kernel.secret(HMAC-SHA256). Nothing to store, the same on every replica, different per host. - The key file is served at
/<key>.txtby the bundle's route. - The whole catalogue is announced once, the first time a channel host is active — the go-live. A database row remembers it, so redeploys and new pods do not announce everything again.
What never gets announced
Only the prod kernel environment sends anything; that is not
configurable. Hosts are checked by the library's host policy, which refuses
.localhost, .test, .invalid, .internal, IP addresses and hosts with a
dev, staging, preview (and similar) label — shop.dev.example.com stays
silent even in prod. On a host that does not announce, the key file does not
exist either.
What gets announced
| Change | Announced |
|---|---|
| A product, its translation, variant, price, image or taxon assignment | The product page in every channel it belongs to and every channel locale it has a slug in, plus its main taxon's listing |
| A taxon or its translation | The taxon page in every channel whose menu tree contains it |
| A product or taxon is deleted | Its URLs, resolved before the deletion |
| The go-live | Homepages, enabled products and enabled taxons |
Changes come from Doctrine, so the admin, imports, the API and fixtures are all covered. A disabled product is still announced when it changes — the engine has to re-crawl to learn it is gone. A translation is required: the fallback locale's slug under another locale would be a different page.
Delivery
Changes are queued (koh_indexnow_queue) and sent by koh:indexnow:run, one
request per host. Accepted rows are removed; 429, 5xx, network errors and 403
(key file not reachable yet) wait with exponential backoff from one minute to
six hours, honouring Retry-After, and are given up after eight attempts; 400
and 422 are removed and logged, and the command exits with status 1.
HTTP goes through the application's http_client with a ten-second timeout, a
twenty-second ceiling and no redirects.
There is deliberately no Messenger message: a queue without a consumer loses messages, and a synchronous transport would put a search engine's latency into the admin's save.
Commands
bin/console koh:indexnow:status # per channel host: active or why not, key file URL, queue bin/console koh:indexnow:run # the worker bin/console koh:indexnow:submit-all # queue the catalogue again, all or the given channels
Configuration
Everything is optional.
koh_indexnow: enabled: true key: '' # empty: derived per host from kernel.secret channels: [] # channel codes; empty: all enabled channels denied_host_suffixes: [] # further hosts never announced include_homepage: true include_main_taxon: true routes: # only if the shop replaced Sylius' routes product: sylius_shop_product_show taxon: sylius_shop_product_index homepage: sylius_shop_homepage
Independence
An independent implementation, written from the IndexNow specification.
Licence
MIT. See LICENSE.