Plugin Grav Google Indexing : soumettre automatiquement ses pages à Google

⚡ En bref
Après le plugin IndexNow (Bing/Yandex), ce second plugin complète le pipeline SEO en soumettant les pages modifiées directement à l’API Google Indexing — sans dépendance Composer, avec un JWT RS256 signé en PHP pur à partir d’un service account Google Cloud.
Le code est disponible sur GitHub :
- 🔌 Plugin : jmrGrav/grav-plugin-google-indexing
🧠 Pourquoi
IndexNow couvre Bing et Yandex, mais Google ne supporte pas IndexNow. Pour notifier Google instantanément, il faut passer par son API dédiée : Google Indexing API v3.
Sans ce plugin :
- Google découvre les pages nouvelles ou modifiées uniquement lors de son prochain crawl
- Le délai peut aller de quelques heures à plusieurs jours
- Aucun feedback sur la prise en compte de la soumission
Avec ce plugin, chaque save déclenche une soumission immédiate avec confirmation HTTP 200 dans les logs.
🔧 Ce qui a été fait
📁 Structure du plugin
/var/www/grav/user/plugins/google-indexing/
├── google-indexing.php ← logique principale
├── blueprints.yaml ← définition des champs admin
└── google-indexing.yaml ← valeurs par défaut🔑 Prérequis Google Cloud
Trois étapes côté Google avant de pouvoir utiliser l’API :
- Créer un projet Google Cloud
- Activer l’Indexing API dans le projet
- Créer un Service Account et télécharger le fichier JSON de credentials
- Ajouter le service account comme Propriétaire dans Google Search Console
⚠️ Le rôle doit être Propriétaire — pas “Accès total” (= simple utilisateur). L’API retourne
403 PERMISSION_DENIEDavec un rôle utilisateur.
| Rôle Search Console | Résultat API |
|---|---|
| Utilisateur complet | ❌ HTTP 403 PERMISSION_DENIED |
| Propriétaire | ✅ HTTP 200 |
🔐 Stockage des credentials
Le fichier JSON est stocké hors du répertoire Grav, accessible uniquement par www-data :
sudo mkdir -p /etc/grav-google-indexing
sudo chown www-data:www-data /etc/grav-google-indexing
sudo chmod 700 /etc/grav-google-indexing
sudo cp service-account.json /etc/grav-google-indexing/
sudo chown www-data:www-data /etc/grav-google-indexing/service-account.json
sudo chmod 600 /etc/grav-google-indexing/service-account.json🔍 Hooks — identiques au plugin IndexNow
public static function getSubscribedEvents(): array
{
return [
'onAdminAfterSave' => ['onAdminAfterSave', 0],
'onMcpAfterSave' => ['onMcpAfterSave', 0],
];
}⚙️ JWT RS256 en PHP pur — sans Composer
L’API Google Indexing nécessite un access token OAuth2. La lib google/auth n’étant pas disponible dans Grav, le JWT est généré manuellement.
⚠️ Attention au base64url (pas base64 standard) — les
+deviennent-, les/deviennent_, et les=sont supprimés.
private function base64url(string $data): string
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
private function getGoogleAccessToken(): ?string
{
$credentials = json_decode(
file_get_contents('/etc/grav-google-indexing/service-account.json'),
true
);
$now = time();
$header = $this->base64url(json_encode(['alg' => 'RS256', 'typ' => 'JWT']));
$payload = $this->base64url(json_encode([
'iss' => $credentials['client_email'],
'scope' => 'https://www.googleapis.com/auth/indexing',
'aud' => 'https://oauth2.googleapis.com/token',
'exp' => $now + 3600,
'iat' => $now,
]));
$toSign = $header . '.' . $payload;
openssl_sign($toSign, $signature, $credentials['private_key'], OPENSSL_ALGO_SHA256);
$jwt = $toSign . '.' . $this->base64url($signature);
// Échanger le JWT contre un access token
$ch = curl_init('https://oauth2.googleapis.com/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' => $jwt,
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
return $response['access_token'] ?? null;
}📤 Soumission à l’API
L’API accepte une URL à la fois (pas de bulk comme IndexNow). Les 3 variantes sont soumises en boucle :
private function handleRoute(string $route): void
{
$host = 'arleo.eu';
$urls = [
"https://{$host}{$route}",
"https://{$host}/fr{$route}",
"https://{$host}/en{$route}",
];
$token = $this->getGoogleAccessToken();
if (!$token) {
$this->grav['log']->error('[GoogleIndexing] Impossible d\'obtenir un access token.');
return;
}
foreach ($urls as $url) {
$ch = curl_init('https://indexing.googleapis.com/v3/urlNotifications:publish');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['url' => $url, 'type' => 'URL_UPDATED']),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
"Authorization: Bearer {$token}",
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$this->grav['log']->info("[GoogleIndexing] ✅ OK {$url}");
} else {
$this->grav['log']->warning("[GoogleIndexing] ⚠️ HTTP {$httpCode} {$url} — {$response}");
}
}
}✅ Vérification dans les logs
tail -f /var/www/grav/logs/grav.log | grep -E "IndexNow|GoogleIndexing"
# [GoogleIndexing] ✅ OK https://arleo.eu/grav-plugin-indexnow
# [GoogleIndexing] ✅ OK https://arleo.eu/fr/grav-plugin-indexnow
# [GoogleIndexing] ✅ OK https://arleo.eu/en/grav-plugin-indexnow
# [IndexNow] ✅ Soumission OK (HTTP 200) pour : https://arleo.eu/fr/grav-plugin-indexnow, ...📊 Pipeline SEO complet
Save de page
(admin ou MCP)
│
┌─────────┴─────────┐
│ │
▼ ▼
IndexNow GoogleIndexing
(bulk POST) (JWT RS256)
│ │
▼ ▼
Bing / Yandex Google| Moteur | Plugin | Mécanisme | Statut |
|---|---|---|---|
| Bing / Yandex | IndexNow | Bulk POST api.indexnow.org | ✅ Actif |
| GoogleIndexing | JWT RS256 + indexing.googleapis.com | ✅ Actif |
🏁 Conclusion
Le pipeline SEO est désormais complet — chaque save de page notifie instantanément Google, Bing et Yandex sans aucune intervention manuelle. Le JWT RS256 est généré en PHP pur sans dépendance Composer, ce qui évite tout risque de conflit avec les dépendances Grav existantes.
Pour aller plus loin :
- 💡 Mettre en cache l’access token Google (valable 1h) pour éviter un appel OAuth à chaque save et réduire la latence
- 💡 Ajouter une soumission bulk initiale au démarrage du plugin pour indexer toutes les pages existantes en une seule passe