---
title: "Plugin Grav Google Indexing : soumettre automatiquement ses pages à Google"
description: "Création d'un plugin Grav from scratch pour soumettre automatiquement les pages modifiées à l'API Google Indexing via un JWT RS256 et un service account Google Cloud."
url: "https://www.arleo.eu/posts/grav-plugin-google-indexing/"
language: "fr"
datePublished: "2026-04-17T14:00:00+02:00"
dateModified: "2026-05-15T23:51:25+02:00"
tags: ["grav","seo","google"]
categories: ["plugins"]
contentSignal: "ai-train=no, search=yes, ai-input=yes"
---

# Plugin Grav Google Indexing : soumettre automatiquement ses pages à Google
Création d'un plugin Grav from scratch pour soumettre automatiquement les pages modifiées à l'API Google Indexing via un JWT RS256 et un service account Google Cloud.



## ⚡ 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](https://github.com/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](https://developers.google.com/search/apis/indexing-api/v3/prereqs).

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 :

1. Créer un projet Google Cloud
2. Activer l'**Indexing API** dans le projet
3. Créer un **Service Account** et télécharger le fichier JSON de credentials
4. 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_DENIED` avec 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` :

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

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

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

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

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



## Tags

- grav
- seo
- google

## Categories

- plugins
