Contenu

Plugin Grav IndexNow : soumettre automatiquement ses pages aux moteurs de recherche

⚡ En bref

Grav ne dispose d’aucun plugin IndexNow dans son catalogue officiel. Ce plugin maison comble ce manque en soumettant automatiquement les URLs modifiées à api.indexnow.org à chaque sauvegarde — que ce soit via l’admin Grav ou via le plugin MCP — sans intervention manuelle, sans cron, sans dépendance externe.

Le code est disponible sur GitHub :

🧠 Pourquoi

IndexNow est un protocole ouvert permettant de notifier instantanément les moteurs de recherche compatibles (Bing, Yandex) qu’une page a été créée ou modifiée. Sans lui, les moteurs attendent leur prochain passage de crawler — qui peut prendre des heures ou des jours.

Plusieurs raisons ont motivé ce développement :

  • Aucun plugin natif : contrairement à WordPress ou Shopify, Grav n’a pas de plugin IndexNow dans son catalogue
  • Cloudflare Crawler Hints déjà actif mais limité aux invalidations de cache — pas aux modifications de contenu
  • Contrôle total : soumettre explicitement les 3 variantes d’URL (/route, /fr/route, /en/route) à chaque save
  • Traçabilité : chaque soumission est loggée dans grav.log avec le code HTTP retourné

🔧 Ce qui a été fait

📁 Structure du plugin

Un plugin Grav minimal se compose de 3 fichiers :

/var/www/grav/user/plugins/indexnow/
├── indexnow.php          ← logique principale
├── blueprints.yaml       ← définition des champs admin
└── indexnow.yaml         ← valeurs par défaut

🔍 Trouver le bon hook admin

Le premier obstacle a été d’identifier l’événement Grav déclenché lors d’un save dans l’admin. Le hook onPageSave — intuitif mais inexistant — ne fonctionne pas. La recherche dans le code source a révélé le bon candidat :

grep -rh "fireEvent(" /var/www/grav/system/src/ | sort -u
# → onAdminAfterSave émis par AdminController.php après chaque save réussi
Hook testéRésultat
onPageSave❌ N’existe pas dans Grav admin
onAdminSave⚠️ Émis avant la validation
onAdminAfterSave✅ Émis après chaque save réussi

⚙️ Code du plugin

public static function getSubscribedEvents(): array
{
    return [
        'onAdminAfterSave' => ['onAdminAfterSave', 0],
        'onMcpAfterSave'   => ['onMcpAfterSave', 0],
    ];
}

🐛 Bug 1 : $this->config() vs $this->grav['config']

La méthode $this->config() dans un plugin Grav retourne un array, pas un objet Config. Appeler ->get() dessus provoque une erreur fatale :

// ❌ Incorrect — retourne un array, pas un objet Config
$config = $this->config();
$key    = $config->get('plugins.indexnow.key');

// ✅ Correct
$config = $this->grav['config'];
$key    = $config->get('plugins.indexnow.key');

🐛 Bug 2 : le plugin MCP bypasse le hook admin

Après les premiers tests, un problème est apparu : les pages créées ou modifiées via le plugin MCP (create_page, update_page) ne déclenchaient pas onAdminAfterSave. Ce hook n’est émis que par PageObject.php dans le contexte admin — les appels programmatiques via MCP le bypasse complètement.

Diagnostic :

# Aucun fireEvent dans le plugin MCP
grep -r "fireEvent" /var/www/grav/user/plugins/mcp-server/mcp-server.php
# → (aucun résultat)

# onAdminAfterSave conditionné à isAdminSite()
grep -r "onAdminAfterSave" /var/www/grav/system/src/Grav/Common/Flex/Types/Pages/PageObject.php
# → if ($this->isAdminSite()) { $grav->fireEvent('onAdminAfterSave', ...) }

Solution — event custom onMcpAfterSave :

Plutôt que de réutiliser onAdminAfterSave (qui nécessite un objet PageInterface pas toujours disponible après un write MCP), un event custom a été ajouté directement dans le plugin MCP après chaque opération réussie :

// Dans mcp-server.php — helper ajouté après toolCreatePage et toolUpdatePage
private function notifyPageSaved(string $route): void
{
    $grav = \Grav\Common\Grav::instance();
    $grav->fireEvent('onMcpAfterSave', new \RocketTheme\Toolbox\Event\Event([
        'route' => $route,
    ]));
}

Le plugin IndexNow écoute cet event et extrait la route directement depuis le 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);
}

✅ Résultat final — deux contextes couverts

ContexteHookStatut
Save via admin GravonAdminAfterSave✅ Actif
create_page / update_page via MCPonMcpAfterSave✅ Actif

📋 Fichier de clé IndexNow

Le fichier clé doit être accessible publiquement à la racine du site :

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

✅ Vérification dans les logs

tail -f /var/www/grav/logs/grav.log | grep -i indexnow
# [IndexNow] ✅ Soumission OK (HTTP 200) pour :
# https://arleo.eu/fr/grav-plugin-indexnow,
# https://arleo.eu/en/grav-plugin-indexnow,
# https://arleo.eu/grav-plugin-indexnow

🏁 Conclusion

Le plugin couvre désormais les deux contextes de modification : l’admin Grav via onAdminAfterSave et le plugin MCP via l’event custom onMcpAfterSave. Chaque sauvegarde déclenche une soumission immédiate des 3 variantes d’URL à IndexNow, avec traçabilité complète dans grav.log. Le délai d’indexation par Bing passe de plusieurs jours à quelques minutes.

Pour aller plus loin :

  • 💡 Ajouter une soumission bulk au démarrage du plugin pour indexer toutes les pages existantes en une seule requête
  • 💡 Étendre la soumission à d’autres moteurs compatibles IndexNow (Naver, Seznam) via leurs endpoints dédiés