Grav IndexNow Plugin: automatically submit pages to search engines

โก In short
Grav has no IndexNow plugin in its official catalog. This homemade plugin fills the gap by automatically submitting modified URLs to api.indexnow.org on every page save โ whether through the Grav admin or the MCP plugin โ no manual intervention, no cron job, no external dependency.
Source code available on GitHub:
- ๐ Plugin: jmrGrav/grav-plugin-indexnow
๐ง Why
IndexNow is an open protocol that instantly notifies compatible search engines (Bing, Yandex) when a page is created or modified. Without it, search engines wait for their next crawler pass โ which can take hours or days.
Several reasons motivated this development:
- No native plugin: unlike WordPress or Shopify, Grav has no IndexNow plugin in its catalog
- Cloudflare Crawler Hints already active but limited to cache invalidations โ not content modifications
- Full control: explicitly submit all 3 URL variants (
/route,/fr/route,/en/route) on every save - Traceability: every submission is logged in
grav.logwith the returned HTTP code
๐ง What was done
๐ Plugin structure
A minimal Grav plugin consists of 3 files:
/var/www/grav/user/plugins/indexnow/
โโโ indexnow.php โ main logic
โโโ blueprints.yaml โ admin field definitions
โโโ indexnow.yaml โ default values๐ Finding the right admin hook
The first obstacle was identifying the Grav event triggered when saving a page in the admin. The onPageSave hook โ intuitive but nonexistent โ doesn’t work. Searching the source code revealed the right candidate:
grep -rh "fireEvent(" /var/www/grav/system/src/ | sort -u
# โ onAdminAfterSave emitted by AdminController.php after each successful save| Hook tested | Result |
|---|---|
onPageSave | โ Does not exist in Grav admin |
onAdminSave | โ ๏ธ Emitted before validation |
onAdminAfterSave | โ Emitted after each successful save |
โ๏ธ Plugin code
public static function getSubscribedEvents(): array
{
return [
'onAdminAfterSave' => ['onAdminAfterSave', 0],
'onMcpAfterSave' => ['onMcpAfterSave', 0],
];
}๐ Bug 1: $this->config() vs $this->grav['config']
The $this->config() method in a Grav plugin returns an array, not a Config object. Calling ->get() on it causes a fatal error:
// โ Wrong โ returns an array, not a Config object
$config = $this->config();
$key = $config->get('plugins.indexnow.key');
// โ
Correct
$config = $this->grav['config'];
$key = $config->get('plugins.indexnow.key');๐ Bug 2: MCP plugin bypasses the admin hook
After initial testing, a problem appeared: pages created or modified via the MCP plugin (create_page, update_page) did not trigger onAdminAfterSave. This hook is only emitted by PageObject.php in the admin context โ programmatic calls via MCP bypass it entirely.
Diagnosis:
# No fireEvent in the MCP plugin
grep -r "fireEvent" /var/www/grav/user/plugins/mcp-server/mcp-server.php
# โ (no results)
# onAdminAfterSave conditioned on isAdminSite()
grep -r "onAdminAfterSave" /var/www/grav/system/src/Grav/Common/Flex/Types/Pages/PageObject.php
# โ if ($this->isAdminSite()) { $grav->fireEvent('onAdminAfterSave', ...) }Solution โ custom onMcpAfterSave event:
Rather than reusing onAdminAfterSave (which requires a PageInterface object not always available after an MCP write), a custom event was added directly in the MCP plugin after each successful operation:
// In mcp-server.php โ helper added after toolCreatePage and toolUpdatePage
private function notifyPageSaved(string $route): void
{
$grav = \Grav\Common\Grav::instance();
$grav->fireEvent('onMcpAfterSave', new \RocketTheme\Toolbox\Event\Event([
'route' => $route,
]));
}The IndexNow plugin listens to this event and extracts the route directly from the payload:
public function onMcpAfterSave(Event $event): void
{
$route = $event['route'] ?? null;
if (!$route) return;
if (in_array($route, self::EXCLUDED_ROUTES, true)) return;
if (str_starts_with($route, '/tag')) return;
$this->submitToIndexNow($route);
}โ Final result โ two contexts covered
| Context | Hook | Status |
|---|---|---|
| Save via Grav admin | onAdminAfterSave | โ Active |
create_page / update_page via MCP | onMcpAfterSave | โ Active |
๐ IndexNow key file
The key file must be publicly accessible at the site root:
curl -I https://arleo.eu/bf6faf5563914fe7bb18d429976b182d.txt
# โ HTTP/2 200โ Verification in logs
tail -f /var/www/grav/logs/grav.log | grep -i indexnow
# [IndexNow] โ
Submission OK (HTTP 200):
# https://arleo.eu/fr/grav-plugin-indexnow,
# https://arleo.eu/en/grav-plugin-indexnow,
# https://arleo.eu/grav-plugin-indexnow๐ Conclusion
The plugin now covers both modification contexts: the Grav admin via onAdminAfterSave and the MCP plugin via the custom onMcpAfterSave event. Every save triggers an immediate submission of all 3 URL variants to IndexNow, with full traceability in grav.log. Bing indexing time drops from several days to a few minutes.
To go further:
- ๐ก Add a bulk submission on plugin startup to index all existing pages in a single request
- ๐ก Extend submission to other IndexNow-compatible engines (Naver, Seznam) via their dedicated endpoints