URL Inspection API : automatiser le diagnostic d'indexation

Le problème : 8 000 pages, et aucune visibilité sur leur statut d'indexation en temps réel

Un site e-commerce de 8 000 fiches produit migre son front-end de Nuxt 2 vers Nuxt 3. Deux semaines après la mise en production, le trafic organique chute de 34 %. L'équipe SEO ouvre Search Console, filtre par couverture, et découvre que 2 400 URLs sont passées en "Discovered – currently not indexed". Le rapport de couverture affiche des données datant de 3 à 5 jours. Le diagnostic manuel, URL par URL via l'outil d'inspection, prendrait des semaines.

C'est exactement le scénario pour lequel Google a exposé l'URL Inspection API en janvier 2022. Cet article détaille comment l'intégrer dans un pipeline de monitoring automatisé, avec du code fonctionnel, les limites réelles de l'API, et les patterns d'architecture qui tiennent en production.

Ce que l'URL Inspection API renvoie (et ce qu'elle ne renvoie pas)

L'API expose par programmation les mêmes données que l'outil d'inspection manuelle de Search Console. Pour chaque URL inspectée, vous obtenez un objet JSON contenant trois blocs principaux :

Le verdict d'indexation

Le champ inspectionResult.indexStatusResult contient le statut d'indexation tel que Google le voit à l'instant T de son dernier passage. Les valeurs possibles de verdict sont : PASS, PARTIAL, FAIL, NEUTRAL. En pratique, c'est surtout coverageState qui vous intéresse — il retourne des valeurs comme Submitted and indexed, Crawled - currently not indexed, Discovered - currently not indexed, ou URL is unknown to Google.

Vous obtenez aussi robotsTxtState, indexingState, pageFetchState, et la date du dernier crawl (lastCrawlTime). Ce dernier champ est critique pour détecter les pages que Googlebot a cessé de visiter.

Le résultat du rendu

inspectionResult.mobileUsabilityResult et les informations sur la version rendue (via inspectionResult.indexStatusResult.referringUrls) donnent des indices sur la façon dont Google a traité le JavaScript. Mais attention : l'API ne renvoie pas le HTML rendu lui-même. Vous n'obtiendrez pas le DOM post-rendering comme avec l'inspection en live dans l'interface web.

Ce qui manque

Trois limitations majeures à garder en tête :

  • Pas d'inspection live : l'API retourne uniquement les données de l'index existant. Il n'existe pas d'équivalent programmatique du bouton "Test Live URL" de l'interface Search Console.
  • Quota de 2 000 requêtes/jour par propriété : c'est une limite dure, non négociable. Pour un site de 50 000 pages, il faut 25 jours pour un scan complet. Votre stratégie d'échantillonnage doit être solide.
  • Pas de données de positionnement : aucune information sur les requêtes, clics ou impressions. Pour ça, vous restez sur l'API Search Analytics.

Authentification et premier appel : le setup complet

Créer les credentials OAuth 2.0

L'URL Inspection API utilise OAuth 2.0 avec un service account. La procédure passe par la Google Cloud Console.

  1. Créez un projet (ou réutilisez un existant).
  2. Activez l'API "Google Search Console API" dans la bibliothèque d'APIs.
  3. Créez un service account avec un fichier de clé JSON.
  4. Ajoutez l'adresse email du service account comme utilisateur de votre propriété Search Console (niveau "Propriétaire" ou "Complet").

Premier appel fonctionnel en TypeScript

Voici un script autonome qui inspecte une URL et affiche le verdict :

import { google } from 'googleapis';
import { readFileSync } from 'fs';

const SCOPES = ['https://www.googleapis.com/auth/webmasters.readonly'];
const KEY_FILE = './service-account-key.json';
const SITE_URL = 'https://www.merciboutique.fr/'; // propriété SC exacte
const INSPECT_URL = 'https://www.merciboutique.fr/robes/robe-soiree-velours-noir';

async function inspectUrl() {
  const auth = new google.auth.GoogleAuth({
    keyFile: KEY_FILE,
    scopes: SCOPES,
  });

  const client = await auth.getClient();
  const searchconsole = google.searchconsole({ version: 'v1', auth: client as any });

  const response = await searchconsole.urlInspection.index.inspect({
    requestBody: {
      inspectionUrl: INSPECT_URL,
      siteUrl: SITE_URL,
      languageCode: 'fr',
    },
  });

  const result = response.data.inspectionResult;
  console.log('Verdict:', result?.indexStatusResult?.verdict);
  console.log('Coverage state:', result?.indexStatusResult?.coverageState);
  console.log('Robots.txt state:', result?.indexStatusResult?.robotsTxtState);
  console.log('Last crawl:', result?.indexStatusResult?.lastCrawlTime);
  console.log('Crawled as:', result?.indexStatusResult?.crawledAs);
  console.log('Canonical:', result?.indexStatusResult?.googleCanonical);
  console.log('User canonical:', result?.indexStatusResult?.userCanonical);

  if (result?.indexStatusResult?.googleCanonical !== INSPECT_URL) {
    console.warn('⚠ Google a choisi une canonical différente de l\'URL inspectée');
  }
}

inspectUrl().catch(console.error);

Le champ siteUrl doit correspondre exactement à la propriété telle qu'elle apparaît dans Search Console — y compris le trailing slash pour les propriétés de type "URL prefix". Pour les propriétés de type Domain, utilisez le format sc-domain:merciboutique.fr.

Lecture du résultat : les champs qui comptent

La réponse JSON complète est volumineuse. Voici les champs à extraire en priorité pour un pipeline de monitoring :

Champ Usage
coverageState Statut d'indexation — le KPI principal
lastCrawlTime Détecte les pages "abandonnées" par Googlebot
robotsTxtState Vérifie qu'un déploiement n'a pas cassé le robots.txt
pageFetchState Repère les erreurs serveur (5xx, timeouts)
googleCanonical vs userCanonical Détecte les conflits de canonical
crawledAs Confirme que Google crawle en mobile-first
referringUrls Identifie les URLs internes qui pointent vers la page

Le champ googleCanonical mérite une attention particulière. Si Google choisit une canonical différente de celle que vous déclarez, votre page est de facto dé-indexée au profit de l'autre. C'est un signal critique, souvent silencieux, que l'API permet de détecter à grande échelle. Pour approfondir la logique des canonicals, voir notre guide sur les URL canoniques.

Architecture d'un pipeline de monitoring à 2 000 requêtes/jour

Le quota de 2 000 requêtes/jour est la contrainte architecturale centrale. Vous ne pouvez pas tout inspecter tout le temps. Il faut prioriser.

Stratégie de tiering

Divisez vos URLs en trois tiers :

Tier 1 — URLs critiques (inspectées quotidiennement) : vos 200-300 pages à plus fort trafic organique. Pages catégories principales, landing pages SEO, homepage. Ces URLs représentent souvent 60-70 % du trafic total.

Tier 2 — URLs importantes (inspectées hebdomadairement) : les 1 500-2 000 pages suivantes. Fiches produit à bon trafic, pages de blog à forte autorité. Rotation sur 7 jours = ~250 URLs/jour.

Tier 3 — Long tail (inspectées mensuellement ou sur événement) : le reste du catalogue. Inspectées en rotation lente, ou déclenchées par un événement (déploiement, modification du sitemap, perte de trafic détectée).

Pour un site de 8 000 pages, le budget quotidien se répartit ainsi :

  • Tier 1 : 300 URLs/jour (fixes)
  • Tier 2 : 280 URLs/jour (rotation sur 7 jours = 1 960 URLs couvertes par semaine)
  • Tier 3 : 1 420 URLs/jour (rotation sur ~4 jours pour couvrir les 5 700 restantes)
  • Total : 2 000/jour

Script de rotation avec file d'attente

import { readFileSync, writeFileSync, existsSync } from 'fs';

interface QueueState {
  tier1: string[];
  tier2: { urls: string[]; cursor: number };
  tier3: { urls: string[]; cursor: number };
  lastRun: string;
}

function loadQueue(path: string): QueueState {
  if (existsSync(path)) {
    return JSON.parse(readFileSync(path, 'utf-8'));
  }
  throw new Error('Queue state file not found. Run init first.');
}

function getDailyBatch(state: QueueState, dailyQuota: number = 2000): string[] {
  const batch: string[] = [];

  // Tier 1 : toujours incluses
  batch.push(...state.tier1);
  let remaining = dailyQuota - state.tier1.length;

  // Tier 2 : rotation
  const tier2BatchSize = Math.min(280, remaining);
  const tier2Start = state.tier2.cursor;
  const tier2Slice = circularSlice(state.tier2.urls, tier2Start, tier2BatchSize);
  batch.push(...tier2Slice);
  state.tier2.cursor = (tier2Start + tier2BatchSize) % state.tier2.urls.length;
  remaining -= tier2Slice.length;

  // Tier 3 : rotation avec le reste du quota
  const tier3Start = state.tier3.cursor;
  const tier3Slice = circularSlice(state.tier3.urls, tier3Start, remaining);
  batch.push(...tier3Slice);
  state.tier3.cursor = (tier3Start + remaining) % state.tier3.urls.length;

  state.lastRun = new Date().toISOString();
  return batch;
}

function circularSlice(arr: string[], start: number, count: number): string[] {
  const result: string[] = [];
  for (let i = 0; i < count && i < arr.length; i++) {
    result.push(arr[(start + i) % arr.length]);
  }
  return result;
}

// Usage
const state = loadQueue('./queue-state.json');
const todayBatch = getDailyBatch(state);
writeFileSync('./queue-state.json', JSON.stringify(state, null, 2));
console.log(`Batch du jour : ${todayBatch.length} URLs`);

Déclenchement sur événement

Le tiering statique ne suffit pas. Votre pipeline doit aussi pouvoir déclencher des inspections réactives :

  • Post-déploiement : un hook CI/CD qui identifie les URLs modifiées (via le diff des routes ou du sitemap) et les injecte en priorité dans la file d'attente du lendemain.
  • Perte de trafic soudaine : un cron qui compare les clics quotidiens via l'API Search Analytics. Si une URL perd plus de 50 % de clics sur 7 jours glissants, elle est ajoutée au batch prioritaire.
  • Nouvelles URLs dans le sitemap : un diff quotidien du sitemap XML qui identifie les nouvelles entrées. Elles remplacent temporairement une partie du quota Tier 3.

Pour les sites qui subissent des problèmes d'indexation récurrents, l'article sur pourquoi Google n'indexe pas vos pages détaille les causes racine les plus fréquentes.

Cas concret : détecter une régression SSR post-déploiement

Revenons au scénario de migration Nuxt 2 → Nuxt 3 de notre e-commerce à 8 000 pages — appelons-le MerciBoutique.

Chronologie de l'incident

Jour J : déploiement de la nouvelle version. Le SSR fonctionne en staging, les tests Lighthouse sont verts.

Jour J+3 : un bug en production fait que le middleware d'authentification intercepte Googlebot sur les pages /collections/* (environ 2 400 URLs). Le serveur renvoie un 200 avec un body quasi vide — le HTML contient le shell de l'app mais aucun contenu produit. Pas de 5xx, pas de redirect, donc aucune alerte côté monitoring infra.

Jour J+5 : le rapport de couverture Search Console commence à montrer des anomalies, mais les données sont agrégées et retardées de 48-72h. L'équipe ne regarde pas ce jour-là.

Jour J+14 : le trafic organique a chuté de 34 %. C'est le rapport hebdomadaire qui déclenche l'alerte.

Comment l'API aurait détecté le problème en 24h

Avec un pipeline d'inspection quotidien, le batch du Jour J+1 aurait inspecté ~100 URLs de /collections/* (Tier 2). Le script aurait détecté :

  • coverageState passant de Submitted and indexed à Crawled - currently not indexed
  • pageFetchState restant SUCCESSFUL (200 OK — c'est pour ça que le monitoring infra n'a rien vu)
  • indexingState passant à INDEXING_ALLOWED mais sans indexation effective

Le pattern "200 OK + contenu vide + dé-indexation progressive" est l'un des plus vicieux en SEO technique, car il ne déclenche aucune alerte classique. Seule l'inspection d'indexation à la source — c'est-à-dire via les données de Google lui-même — permet de le repérer rapidement.

Script d'alerte sur changement de statut

import { PrismaClient } from '@prisma/client';

interface InspectionResult {
  url: string;
  coverageState: string;
  lastCrawlTime: string;
  pageFetchState: string;
  googleCanonical: string | null;
}

const prisma = new PrismaClient();

async function detectRegressions(results: InspectionResult[]) {
  const alerts: string[] = [];

  for (const result of results) {
    const previous = await prisma.inspectionLog.findFirst({
      where: { url: result.url },
      orderBy: { createdAt: 'desc' },
    });

    // Régression d'indexation
    if (
      previous?.coverageState === 'Submitted and indexed' &&
      result.coverageState !== 'Submitted and indexed'
    ) {
      alerts.push(
        `REGRESSION: ${result.url} passée de "Submitted and indexed" à "${result.coverageState}"`
      );
    }

    // Page non crawlée depuis 30 jours
    const lastCrawl = new Date(result.lastCrawlTime);
    const daysSinceCrawl = (Date.now() - lastCrawl.getTime()) / (1000 * 60 * 60 * 24);
    if (daysSinceCrawl > 30) {
      alerts.push(
        `STALE: ${result.url} non crawlée depuis ${Math.round(daysSinceCrawl)} jours`
      );
    }

    // Canonical mismatch
    if (result.googleCanonical && result.googleCanonical !== result.url) {
      alerts.push(
        `CANONICAL MISMATCH: ${result.url} → Google a choisi ${result.googleCanonical}`
      );
    }

    // Persister le résultat
    await prisma.inspectionLog.create({
      data: {
        url: result.url,
        coverageState: result.coverageState,
        lastCrawlTime: result.lastCrawlTime,
        pageFetchState: result.pageFetchState,
        googleCanonical: result.googleCanonical,
      },
    });
  }

  if (alerts.length > 0) {
    await sendSlackAlert(alerts);
  }
}

async function sendSlackAlert(alerts: string[]) {
  const webhook = process.env.SLACK_WEBHOOK_URL!;
  await fetch(webhook, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: `🔍 *URL Inspection Alert* — ${alerts.length} anomalie(s) détectée(s)\n\n${alerts.join('\n')}`,
    }),
  });
}

Ce type de détection de régressions est précisément ce qu'un outil de monitoring comme SEOGard automatise en continu, en croisant les données d'inspection avec les signaux de crawl et les métriques de trafic — sans que vous ayez à maintenir le pipeline vous-même.

Combiner l'API avec d'autres sources de données

L'URL Inspection API seule ne suffit pas. Sa valeur explose quand vous croisez ses résultats avec d'autres signaux.

Croisement avec le sitemap XML

Votre sitemap déclare les URLs que vous voulez indexées. L'API vous dit celles que Google a effectivement indexées. La différence entre les deux est votre "gap d'indexation".

Pour les bonnes pratiques de construction de sitemaps, voir notre guide sitemap XML.

Le calcul est simple : parsez votre sitemap (ou vos sitemaps — un site de 8 000 pages en a probablement plusieurs), extrayez les URLs, et comparez avec le coverageState renvoyé par l'API. Un taux d'indexation en dessous de 85 % sur des pages déclarées dans le sitemap mérite investigation.

Croisement avec les logs serveur

Le lastCrawlTime de l'API donne la vision Google. Vos logs serveur donnent la vision complète (tous les bots, toutes les requêtes). Croisez les deux :

  • Une page crawlée fréquemment par Googlebot (dans vos logs) mais avec un lastCrawlTime ancien dans l'API peut indiquer un problème de rendering — Googlebot visite la page mais ne parvient pas à l'indexer.
  • Une page avec un lastCrawlTime récent mais qui n'apparaît plus dans vos logs peut indiquer que Google utilise une version en cache.

Cette approche est complémentaire de l'analyse de crawl budget — vous identifiez non seulement combien de crawl vous recevez, mais aussi si ce crawl produit effectivement de l'indexation.

Croisement avec Screaming Frog

Screaming Frog permet d'intégrer les données de l'API directement dans un crawl via son connecteur Search Console. Dans la version 19+, allez dans Configuration > API Access > Google Search Console, et activez "URL Inspection". Chaque URL crawlée sera enrichie des colonnes d'indexation.

L'intérêt : vous obtenez dans un seul export le statut HTTP vu par votre crawler, le contenu de la page, les directives meta robots, ET le verdict d'indexation de Google. Les divergences entre ce que votre crawler voit et ce que Google rapporte sont souvent les bugs les plus impactants.

Par exemple, si Screaming Frog voit un <meta name="robots" content="index, follow"> mais que l'API renvoie indexingState: INDEXING_NOT_ALLOWED, vous avez probablement un problème au niveau du robots.txt ou un header HTTP X-Robots-Tag qui contredit la balise meta. Pour démêler ces cas, notre article sur meta robots et ses variantes est une référence utile.

Gestion des erreurs et des cas limites

Rate limiting et retries

L'API renvoie un 429 Too Many Requests quand vous dépassez le quota ou que vous envoyez trop de requêtes en parallèle. Implémentez un exponential backoff :

async function inspectWithRetry(
  searchconsole: any,
  inspectionUrl: string,
  siteUrl: string,
  maxRetries: number = 5
): Promise<any> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await searchconsole.urlInspection.index.inspect({
        requestBody: { inspectionUrl, siteUrl, languageCode: 'fr' },
      });
      return response.data;
    } catch (error: any) {
      if (error.code === 429 || error.code === 503) {
        const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
        console.log(`Rate limited. Retry in ${Math.round(delay)}ms (attempt ${attempt + 1})`);
        await new Promise((resolve) => setTimeout(resolve, delay));
      } else if (error.code === 403) {
        console.error(`Permission denied for ${inspectionUrl}. Check SC property access.`);
        throw error;
      } else {
        throw error;
      }
    }
  }
  throw new Error(`Max retries exceeded for ${inspectionUrl}`);
}

En pratique, espacez vos appels d'au moins 200-300ms pour éviter les 429. Un batch de 2 000 URLs prend environ 10-15 minutes à ce rythme — largement dans les limites d'un cron quotidien.

URLs non reconnues

L'API peut renvoyer URL_NOT_ON_PROPERTY si l'URL inspectée ne correspond pas à la propriété déclarée. Les causes fréquentes :

  • Mismatch http vs https
  • Mismatch www vs non-www
  • Propriété de type "URL prefix" qui ne couvre pas le sous-domaine
  • Trailing slash manquant dans siteUrl

Validez systématiquement que vos URLs sont normalisées avant de les soumettre à l'API.

Pages jamais vues par Google

Pour les URLs nouvellement créées, l'API renvoie verdict: NEUTRAL avec coverageState: URL is unknown to Google. Ce n'est pas une erreur — c'est simplement que Googlebot n'est pas encore passé. Filtrez ces cas de vos alertes de régression pour éviter les faux positifs. Ne déclenchez une alerte que si une URL reste "unknown" plus de 7-10 jours après son ajout au sitemap.

Pour les sites confrontés au problème inverse — trop de pages indexées, diluant la qualité — la problématique d'index bloat est un angle complémentaire à explorer.

Au-delà de l'API : quand l'inspection ne suffit pas

L'URL Inspection API résout le problème de la visibilité sur l'indexation. Elle ne résout pas tout.

Elle ne vous dira pas pourquoi une page est dé-indexée — seulement qu'elle l'est. Le diagnostic des causes (contenu thin, crawl budget épuisé, problème de rendering JavaScript) nécessite d'autres outils. Elle ne vous donnera pas non plus de signal sur les Core Web Vitals en production — pour ça, les données CrUX ou l'API PageSpeed Insights restent indispensables, et notre article sur les Core Web Vitals détaille leur impact réel sur le ranking.

Le vrai pouvoir de cette API est dans l'automatisation de la détection. Détecter une régression d'indexation en 24h au lieu de 14 jours, c'est la différence entre un incident mineur et une perte de revenu à six chiffres sur un e-commerce de taille moyenne. Construisez le pipeline, tiérisez vos URLs, croisez les données avec vos logs et vos crawls Screaming Frog — et vous aurez un système d'alerte SEO que 95 % des sites n'ont pas.

Articles connexes

Crawl7 mars 2026

Pagination SEO : rel=prev/next est mort, quelles alternatives

rel=prev/next n'est plus interprété par Google. Découvrez les stratégies modernes de pagination SEO pour sites volumineux : architecture, crawl et indexation.

Crawl6 mars 2026

Index bloat : identifier et éliminer l'inflation d'index

Diagnostic et résolution de l'index bloat : méthodes concrètes pour réduire les pages inutiles indexées et récupérer du crawl budget.

Crawl5 mars 2026

Pourquoi Google n'indexe pas vos pages : diagnostic complet

Diagnostic technique des problèmes d'indexation Google : crawl, rendu JS, directives, qualité. Méthodes et outils pour identifier et corriger chaque blocage.