Configuration reference¶
Every node of the indexnowkit extension, with its default and what it does. Anything that is not an environment
placeholder is validated when the container is compiled, so a typo fails at cache:clear, not at the first
submission.
# config/packages/indexnowkit.yaml
indexnowkit:
# Master switch. false = collect nothing, submit nothing, register no Doctrine listener.
# Prefer dry_run if you still want the logs and the profiler panel.
enabled: true
# Default IndexNow key, 8-128 characters of [A-Za-z0-9-].
# Generate: bin/console indexnow:key:generate --write-env
key: null # '%env(INDEXNOW_KEY)%'
# Absolute site URL. Needed outside HTTP requests (console commands, Messenger workers) and to
# resolve relative URLs. Required when dispatch is "messenger", and whenever `hosts` is used.
base_url: null # '%env(INDEXNOW_BASE_URL)%'
# Absolute URL of the key file when it is NOT served at https://<host>/<key>.txt.
# Must be on the host of base_url.
key_location: null
# The key before a rotation: /<previous_key>.txt keeps being served while engines re-verify,
# nothing is ever submitted under it. Remove it once the rotation settled.
previous_key: null
# Multi-domain: one entry per additional host. Hosts not listed here use the default key,
# unless strict_hosts is on. An array node cannot come from a single env var, so use
# per-entry %env(...)% placeholders.
hosts:
# example.de:
# key: '%env(INDEXNOW_KEY_DE)%'
# key_location: 'https://example.de/keys/indexnow.txt' # optional
# base_url: 'https://example.de' # console/worker URL generation
# engines: [yandex] # this host only; default: `engines`
# previous_key: '%env(INDEXNOW_KEY_DE_OLD)%' # rotation window
# Refuse URLs of hosts that are neither base_url nor listed in `hosts`, instead of announcing
# them under the default key. Recommended for multi-domain setups, and for any production
# app that is also reachable under a staging or internal hostname (indexnow:check warns).
strict_hosts: false
# Environment names treated as production: no dry-run safety net there, and indexnow:check
# flags dry_run. Replace (not extend) the list when yours is not prod/production.
production_environments: [prod, production]
# URLs longer than this many bytes are skipped as invalid_url. The protocol sets no limit.
max_url_length: 2048
# api = api.indexnow.org, the shared endpoint reaching Yandex, Bing, Naver, Seznam, Yep, Internet Archive and Amazon.
# Name a single engine only to target it, or give a full https endpoint URL (or an alias below).
engines: [api]
# Short names for custom endpoints, usable in engines and hosts.<host>.engines.
engine_aliases: {} # { corp: 'https://index.corp.example/indexnow' }
# locale => host. A rule with `locales` and no `host` generates each locale on its host
# (en on www.example.com, de on example.de). List those hosts in `hosts` with their keys.
locale_hosts: {} # { en: www.example.com, de: example.de }
# auto = messenger when a Messenger transport is configured, otherwise sync.
# sync = submit after the response was sent (kernel.terminate). none = collect, never send.
dispatch: auto # auto | sync | messenger | none
messenger:
# Bus service id. It needs the dispatch_after_current_bus middleware; default buses have it.
bus: messenger.default_bus
# Transport name from framework.messenger.transports. When set, the bundle adds the routing
# for SubmitUrlsMessage itself, so messenger.yaml needs no edit.
transport: null # e.g. 'async'
# Milliseconds: a DelayStamp on every SubmitUrlsMessage. Needs a transport that supports
# delays (doctrine, amqp, redis, sqs); in-memory and sync transports ignore it.
delay: 0
# Service ids of extra stamps added to every message (a FIFO group id, a priority).
stamps: []
batch:
# URLs per request. The protocol maximum is 10000; larger sets are split.
max_urls: 10000
debounce:
# Seconds during which the same URL is not re-submitted. Yandex accepts the same URL at most
# once per 10 minutes. 0 disables debouncing.
per_url: 600
# 'memory' (per process: CLI, tests), 'none', or a PSR-6 cache pool service id shared by all
# processes.
store: cache.app
# Cache key prefix. Give each application sharing one pool its own.
key_prefix: indexnowkit_
throttle:
# Outgoing requests per minute, per process: N workers get N buckets. For a site-wide limit replace
# indexnowkit.throttle (ThrottleInterface) with a shared limiter, e.g. on symfony/rate-limiter + Redis.
max_requests_per_minute: 60
http:
# Seconds. Applied to the client the bundle creates itself.
timeout: 10.0
# Override the indexnowkit-php/<version> User-Agent.
user_agent: null
# Service id of a PSR-18 client OR of a symfony/http-client (including
# framework.http_client.scoped_clients, wrapped automatically). Default: auto-discovery.
# Use a scoped client for proxy, retries or extra headers.
client: null
key_file:
# Serve the key file so engines can verify the key.
enabled: true
# Route path. {key} is required and constrained to the key format.
path: '/{key}.txt'
# Restrict the route to this host pattern (a Symfony route host requirement). Default: any host.
host: null
# Name of the route; rename it when it clashes with an existing one.
route_name: indexnowkit_key_file
# Cache-Control max-age in seconds. Keep it short so a key rotation propagates quickly.
cache_max_age: 300
# Deprecated alias of key_file.enabled.
serve_key_file: null
# Log the request instead of sending it. Switched on automatically outside prod when no key is set.
dry_run: false
logging:
# Monolog channel every bundle service logs to.
channel: indexnow
# URLs listed in one log line (the count is always logged). 0 = no URLs in logs (PII policies).
max_urls: 20
# Consecutive 403s for one host before the log level escalates to critical (the line to page on).
forbidden_escalation: 5
# Bytes of an engine response body kept in a failure log line.
max_body: 300
# Override the level of an outcome. Events and their defaults: ok (debug), pending (info),
# invalid_request (error), unprocessable (warning), rate_limited (warning), server_error (warning),
# unexpected (error), transport (warning), no_key (warning), dry_run (info), disabled (info),
# debounced (debug), invalid_url (warning).
levels: {} # e.g. { debounced: info, rate_limited: error }
resolver:
# How many "via:" hops a rule may follow (Comment -> Post -> Author).
max_via_depth: 3
# How many related objects one "via:" hop may yield; the rest is dropped with a warning.
max_via_fanout: 100
collector:
# Flush as soon as this many URLs were collected in one request or command (0 = only at the
# end). Bounds memory in long imports; the flush goes through the normal dispatcher.
max_urls: 0
# Warn at shutdown about collected URLs that were never flushed.
detect_leaks: true
profiler:
# Register the profiler panel when WebProfilerBundle is present.
enabled: true
flush:
# Listener priority of the kernel.terminate flush. Default -1000: before the profiler (-1024),
# so results land in the panel. Raise or lower it to order against your own terminate listeners.
priority: -1000
# Same for console.terminate and the Messenger WorkerMessageHandledEvent flush.
console_priority: -1024
# Needs indexnowkit/sitemap (composer require indexnowkit/sitemap). Without the package the block is
# accepted as is and ignored; indexnow:check says so.
sitemap:
# Register indexnow:sitemap and the sitemap reader. false = the command does not exist; nothing
# else in the bundle reads sitemaps.
enabled: true
# Sitemap read by indexnow:sitemap when no argument is given. Default: <base_url>/sitemap.xml.
url: null
# Levels of <sitemapindex> followed below the root (0 = the root only).
max_depth: 3
# Documents fetched per run, root included.
max_sitemaps: 1000
# Size cap of one uncompressed sitemap document (protocol maximum 50 MiB). Documents are
# spooled to temp files, never held in memory, so this bounds disk and time rather than RAM.
max_bytes: 52428800
# Follow nested sitemaps on other origins (CDN-hosted parts). Off by default: a sitemap then
# decides which hosts this server fetches from. --allow-foreign-hosts enables it for one run.
allow_foreign_hosts: false
# Where a document is kept while parsing. auto = a temp file, or memory when the temp dir is not
# writable (read-only container; logged once). disk = a temp file or fail. memory = never touch
# the disk (at most max_bytes per document, so memory stays bounded either way).
spool: auto
# Directory for the temp files. Default: sys_get_temp_dir(), i.e. TMPDIR / sys_temp_dir. On a
# readOnlyRootFilesystem point it at an emptyDir or tmpfs mount.
spool_dir: null
# Extra attempts (1 s, 2 s, 4 s apart) when fetching a document fails on the network or with a
# 5xx. 4xx and broken documents are never retried.
fetch_retries: 2
doctrine:
# Hook Doctrine ORM. Needs indexnowkit/doctrine + doctrine/doctrine-bundle.
enabled: true
# Lower than Gedmo so slugs exist before URLs are resolved.
listener_priority: -100
# Restrict the listener and the commit-safety middleware to these DBAL connection names.
# Empty = all connections.
connections: []
Compile-time validation¶
The container fails to build when:
| Rule | Message |
|---|---|
dispatch: messenger without base_url |
a Messenger worker has no request context and would generate http://localhost/... URLs |
a hosts map without base_url |
the default host would be unknown |
strict_hosts: true with neither base_url nor hosts |
there would be no known host at all |
engines: [] |
at least one engine is required |
a literal key outside [A-Za-z0-9-]{8,128} |
|
a literal base_url that is not an absolute http(s) URL |
|
an engines entry that is neither a known engine nor an http(s) URL |
|
key_file.path not starting with / or without {key} |
|
a literal key_location that is not an absolute http(s) URL |
|
a literal previous_key outside [A-Za-z0-9-]{8,128} |
|
a literal http.user_agent containing a line break |
|
logging.levels with an unknown event |
the message lists the known events |
literal sitemap.url that is not an absolute http(s) URL |
|
dispatch: messenger without symfony/messenger installed |
install it, or use dispatch: sync |
a number outside its range: max_url_length ≥ 64, http.timeout ≥ 0.1, batch.max_urls 1–10000, sitemap.max_bytes ≥ 1024, sitemap.max_sitemaps ≥ 1, key_file.cache_max_age ≥ 0, resolver.max_via_fanout ≥ 1, logging.forbidden_escalation ≥ 1 |
the node's own message |
Literal values only. A %env(...)% placeholder is resolved at runtime, so it is skipped here and validated by the
core's Config instead — see the next section.
Core options with no node here: retry.* (Symfony retries through Messenger's retry_strategy, so the core
RetryPolicy is not used by the bundle) and serve_key_file, replaced by key_file.enabled.
Environment placeholders and runtime failures¶
Anything read from an environment variable is only known when the container runs. A bad value there — an empty
INDEXNOW_KEY in production, a malformed INDEXNOW_BASE_URL — used to surface as an exception thrown from a
Doctrine flush or from kernel.terminate.
Instead, the bundle logs one critical line and runs disabled until the value is fixed:
indexnow: invalid configuration, IndexNow is disabled until it is fixed: <error> (run "bin/console indexnow:check")
bin/console indexnow:check prints the same error and exits non-zero, so it belongs in your deployment smoke tests.
The hosts node and environment variables¶
hosts is an array node. Symfony cannot populate a whole array node from a single environment variable, so this
does not work:
indexnowkit:
hosts: '%env(json:INDEXNOW_HOSTS)%' # not supported
List the hosts in YAML and put a placeholder on each key instead:
indexnowkit:
base_url: '%env(INDEXNOW_BASE_URL)%'
hosts:
example.de:
key: '%env(INDEXNOW_KEY_DE)%'
base_url: 'https://example.de'
Per-environment defaults¶
The Flex recipe sets dry_run: true in dev and test. Independently of that, the core turns dry_run on by
itself whenever no key is configured and kernel.environment is not prod or production, so a developer who
never sets INDEXNOW_KEY gets logging instead of a boot failure.
The reverse is worth an alert: dry_run on in production means nothing is being submitted at all.
indexnow:check reports that combination as an error, not a warning.
Service aliases¶
Every replaceable piece is a service with an interface alias, so an application can decorate it.
| Interface | Service id |
|---|---|
IndexNowKit\IndexNowKit (facade) |
indexnowkit |
Config |
indexnowkit.config |
Http\TransportInterface |
indexnowkit.transport (lazy) wrapping indexnowkit.transport.real |
Key\KeyProviderInterface |
indexnowkit.key_provider |
Key\KeyFileResponder |
indexnowkit.key_file_responder |
Url\UrlNormalizerInterface |
indexnowkit.url_normalizer |
Url\UrlResolverInterface |
indexnowkit.url_resolver |
Url\GuardedUrlResolver |
indexnowkit.guarded_url_resolver |
Url\ObjectChangeHandler |
indexnowkit.change_handler |
Url\RouteUrlResolverInterface |
indexnowkit.route_url_resolver |
Url\ResolverLocatorInterface |
indexnowkit.resolver_locator |
Attribute\AttributeReaderInterface |
indexnowkit.attribute_reader |
ClientInterface |
indexnowkit.client |
SubmitterInterface |
indexnowkit.submitter |
Collector\CollectorInterface |
indexnowkit.collector |
Debounce\DebounceStoreInterface |
indexnowkit.debounce_store |
Throttle\ThrottleInterface |
indexnowkit.throttle |
Dispatch\DispatcherInterface |
indexnowkit.dispatcher |
Sitemap\SitemapSourceInterface (and Sitemap\SitemapReader) |
indexnowkit.sitemap_reader (only with sitemap.enabled) |
Console\SubjectLoaderInterface (indexnowkit/console) |
indexnowkit.entity_loader (only with Doctrine) |
Check\CheckerInterface |
indexnowkit.checker (runs every Check\CheckInterface service, autoconfigured with the indexnowkit.check tag) |
Adapter\SubmitterFactoryInterface (core) |
indexnowkit.command_submitter_factory |
Console\ResultFormatterInterface (indexnowkit/console) |
indexnowkit.result_formatter |
Console\Vocabulary, Console\*Runner (indexnowkit/console) |
indexnowkit.console.vocabulary, indexnowkit.console.{check,submit,submit_entity,explain,sitemap,key_generate} |
Check\WiringCheck, core Check\SitemapSpoolCheck |
indexnowkit.check.wiring, indexnowkit.check.sitemap_spool (tagged indexnowkit.check) |
Routing\KeyFileRouteLoader |
indexnowkit.key_file_routes |
Only indexnowkit, IndexNowKit\IndexNowKit and the key file controller are public; inject the rest by type where you need them. How to
decorate or replace each one: extending.md.
Container parameters¶
indexnowkit.dispatch (the resolved mode, after auto), indexnowkit.messenger.transport,
indexnowkit.messenger_routed, indexnowkit.doctrine_hooked, indexnowkit.key_file.path,
indexnowkit.key_file.host, indexnowkit.key_file.route_name, indexnowkit.log_channel. indexnow:check prints the first, third and fourth.