kreativsoehne / sulu-indexnow-bundle
Instantly notifies Bing, Yandex, Seznam and Naver about published, updated and unpublished Sulu content via IndexNow.
Package info
github.com/kreativsoehne/sulu-indexnow-bundle
Type:symfony-bundle
pkg:composer/kreativsoehne/sulu-indexnow-bundle
Requires
- php: ^8.2
- psr/log: ^1.0 || ^2.0 || ^3.0
- sulu/sulu: ^2.6.23
- symfony/config: ^6.1 || ^7.0
- symfony/console: ^6.1 || ^7.0
- symfony/dependency-injection: ^6.1 || ^7.0
- symfony/event-dispatcher: ^6.1 || ^7.0
- symfony/filesystem: ^6.1 || ^7.0
- symfony/http-client: ^6.1 || ^7.0
- symfony/http-kernel: ^6.1 || ^7.0
- symfony/yaml: ^6.1 || ^7.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.40
- jackalope/jackalope-doctrine-dbal: ^1.7 || ^2.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.2
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^10.5 || ^11.0
- rector/rector: ^2.0
- sulu/article-bundle: ^2.6
Suggests
- sulu/article-bundle: Submits articles to IndexNow as well when they are published or unpublished
This package is not auto-updated.
Last update: 2026-08-06 15:39:52 UTC
README
Tells Bing, Yandex, Seznam and Naver about your content the moment you publish it, instead of waiting for the next crawl. Pages and articles are submitted automatically; a console command covers the initial bulk submission.
IndexNow is an open protocol. Google does not take part — there, sitemaps and crawling stay the way to go.
Requirements
- PHP 8.2+
- Sulu 2.6.23+ on Symfony 6.1+ — everything below is held back by security advisories and cannot be installed without overriding them
sulu/article-bundleis optional; articles are picked up automatically when it is installed
Installation
composer require kreativsoehne/sulu-indexnow-bundle
Register the bundle in config/bundles.php (Symfony Flex does this for you):
return [ // … KreativSoehne\SuluIndexNowBundle\SuluIndexNowBundle::class => ['all' => true], ];
Generate a key and the key file that proves you control the domain:
bin/console indexnow:key:generate
This writes public/<key>.txt and prints the configuration to copy:
# config/packages/sulu_index_now.yaml sulu_index_now: key: '8a4f2c1e9b7d4a3f8e6c5b2d1a9f7e3c'
Deploy the key file together with the configuration — without a publicly
readable https://<host>/<key>.txt every submission is rejected.
Finally, announce what you already have:
bin/console indexnow:submit --sitemap=https://example.com/sitemap.xml
That's it. From now on every publish is reported automatically.
Configuration
Every option with its default:
sulu_index_now: # Master switch. When false nothing is sent, the commands stay available. enabled: true # The key. Not a secret — it has to be publicly readable. key: '' # Absolute URL of the key file, if it is not served from the document root. key_location: null # Submit automatically on publish and unpublish. auto_submit: true # Restrict submissions to these hosts. Empty means every host Sulu resolves. hosts: [] # Scheme used to build absolute URLs and the key file location. scheme: 'https' # The default endpoint forwards to every participating search engine. endpoint: 'https://api.indexnow.org/indexnow' # HTTP timeout in seconds. timeout: 10 # Where indexnow:key:generate writes the key file. public_dir: '%kernel.project_dir%/public'
The key is usually worth keeping out of the config file:
sulu_index_now: key: '%env(INDEXNOW_KEY)%'
Multiple webspaces
URLs are grouped by host and sent in one request per host, so a multi-webspace
installation works out of the box — as long as the same key file is reachable
under every host. Use hosts to restrict submissions to the domains you
actually want to announce:
sulu_index_now: hosts: ['www.example.com', 'shop.example.com']
What gets submitted
| Event | Submitted |
|---|---|
| Page published | yes, unless noindex or "hide in sitemap" is set |
| Page unpublished | yes, so the URL leaves the index |
| Article published | yes, unless noindex or "hide in sitemap" is set |
| Article unpublished | yes |
Articles are submitted for their main webspace and every additional webspace they are routed in.
Commands
# One or more URLs bin/console indexnow:submit https://example.com/blog/hello-world # Everything in a sitemap or sitemap index bin/console indexnow:submit --sitemap=https://example.com/sitemap.xml # See what would be sent bin/console indexnow:submit --sitemap=https://example.com/sitemap.xml --dry-run # Create a new key and key file bin/console indexnow:key:generate --force
Before submitting, indexnow:submit verifies that the key file is publicly
reachable for every host involved and aborts otherwise. Use --skip-key-check
if you know better.
How it works
The bundle listens to Sulu's domain events (PagePublishedEvent,
PageUnpublishedEvent, and their article counterparts), resolves the absolute
URL through the webspace manager, and buffers it. The actual HTTP request goes
out on kernel.terminate — after the response has reached the editor. Two
consequences worth knowing:
- Publishing never waits for the search engines.
- If the endpoint is down, it is logged (channel
sulu_index_now) and publishing carries on. Nothing is retried; the next publish or a manualindexnow:submitfixes it.
Submitting your own URLs
Anything routed outside of pages and articles — custom entities with a
sulu/route-bundle route, for instance — can be submitted through the same
service:
<?php namespace App\Event; use KreativSoehne\SuluIndexNowBundle\Submitter\UrlCollector; class EventPublishedSubscriber { public function __construct(private readonly UrlCollector $collector) { } public function onPublished(): void { // Sent together with everything else on kernel.terminate. $this->collector->add('https://example.com/events/summer-party'); } }
Or submit immediately, bypassing the buffer:
<?php namespace App\Event; use KreativSoehne\SuluIndexNowBundle\Submitter\IndexNowSubmitterInterface; class EventPublisher { public function __construct(private readonly IndexNowSubmitterInterface $submitter) { } public function publish(): void { $this->submitter->submit(['https://example.com/events/summer-party']); } }
Both services are registered under their class name and can be autowired
(UrlCollector, IndexNowSubmitterInterface).
Troubleshooting
Nothing is submitted. Check enabled, that key is set, and that the URL's
host passes the hosts allowlist. indexnow:submit <url> --dry-run tells you
whether a URL survives filtering.
The endpoint answers 403. The key file is missing, unreachable or its content does not match the key. It must contain the key and nothing else.
The endpoint answers 422. The submitted URLs do not belong to the host in
the payload, usually a scheme or webspace URL mismatch.
URLs point at the wrong domain. Sulu resolves URLs per environment. Check
the <environment type="prod"> URLs in your webspace configuration — that is
what the resolver uses.
Development
composer install composer test # PHPUnit composer lint # php-cs-fixer, PHPStan and Rector, all in --dry-run composer fix # applies what Rector and php-cs-fixer suggest
License
MIT — see LICENSE.