---
title: "Grav IndexNow Plugin: automatically submit pages to search engines"
description: "Building a Grav plugin from scratch to automatically submit modified pages to IndexNow (Bing, Yandex) using the onAdminAfterSave hook."
url: "https://www.arleo.eu/en/posts/grav-plugin-indexnow/"
language: "en"
datePublished: "2026-04-17T12:00:00+02:00"
dateModified: "2026-05-15T23:51:25+02:00"
tags: ["grav","seo","indexnow"]
categories: ["plugins"]
contentSignal: "ai-train=no, search=yes, ai-input=yes"
---

# Grav IndexNow Plugin: automatically submit pages to search engines
Building a Grav plugin from scratch to automatically submit modified pages to IndexNow (Bing, Yandex) using the onAdminAfterSave hook.



## ⚡ 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](https://github.com/jmrGrav/grav-plugin-indexnow)

## 🧠 Why

[IndexNow](https://www.indexnow.org/) 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.log` with 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:

```bash
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

```php
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:

```php
// ❌ 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:**

```bash
# 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:

```php
// 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:

```php
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:

```bash
curl -I https://arleo.eu/bf6faf5563914fe7bb18d429976b182d.txt
# → HTTP/2 200
```

### ✅ Verification in logs

```bash
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



## Tags

- grav
- seo
- indexnow

## Categories

- plugins
