[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$ftyQlBET_IB08X3tCiS2mb_5-ClplIUYFBspvYKrkREk":3,"$fsfymn3fqF4z7L07yCfTOZJ8AXP0xwmhn_fQWuUP0j8Q":24},{"_id":4,"slug":5,"__v":6,"author":7,"body":8,"canonical":9,"category":10,"createdAt":11,"date":12,"description":13,"htmlContent":14,"image":15,"imageAlt":15,"readingTime":16,"tags":17,"title":22,"updatedAt":23},"69d69828aa6b273b0c882b37","search-console-api-automatiser-le-reporting-seo",0,"Equipe Seogard","L'interface web de Google Search Console plafonne à 1 000 lignes exportables. Pour un site e-commerce de 20 000 pages produit, ça revient à analyser 5 % de vos données de performance. L'API Search Console supprime cette limite et ouvre la porte à des rapports que l'interface ne permettra jamais : croisement requêtes × pages × devices sur 16 mois, détection automatique de chutes de CTR, alerting sur les régressions de positionnement.\n\n## Comprendre les limites de l'API (et les contourner)\n\nL'API Search Console (officiellement \"Search Analytics API\", intégrée à l'API Google Search Console v1) n'est pas un miroir parfait de l'interface. Plusieurs contraintes techniques conditionnent la façon dont vous allez structurer vos appels.\n\n### Quotas et rate limiting\n\nGoogle impose un quota de **1 200 requêtes par minute par projet** et **50 000 requêtes par jour**. Pour la majorité des sites, c'est largement suffisant. Le problème survient quand vous interrogez plusieurs propriétés (multi-sites, versions www/non-www, domaines internationaux) depuis le même projet GCP.\n\nChaque appel à `searchAnalytics.query` retourne au maximum **25 000 lignes**. Si votre combinaison dimensions/filtres dépasse ce seuil, les données sont tronquées silencieusement — pas d'erreur, pas de warning. C'est le piège classique.\n\n### La fenêtre de données\n\nL'API expose les données des **16 derniers mois**, avec un délai de 2 à 3 jours. Les données des 48 dernières heures sont instables — Google les recalcule. Planifier vos pulls à J-3 minimum évite les écarts entre deux extractions successives.\n\n### Dimensions disponibles\n\nCinq dimensions sont combinables : `query`, `page`, `country`, `device`, `searchAppearance`. Vous pouvez en combiner jusqu'à trois dans un seul appel. Au-delà, l'API refuse la requête. Ce qui signifie que pour obtenir une vue `query × page × device × country`, il faut deux appels distincts et un join côté client.\n\nUn point souvent ignoré dans la documentation : la dimension `searchAppearance` est **mutuellement exclusive** avec les autres combinaisons de trois dimensions. Si vous l'utilisez, limitez-vous à deux dimensions supplémentaires.\n\n## Configurer l'authentification OAuth2\n\nAvant toute chose, vous avez besoin d'un projet Google Cloud Platform avec l'API Search Console activée. La documentation officielle détaille la procédure : [Google Search Console API - Getting Started](https://developers.google.com/webmaster-tools/v1/how-tos/authorizing).\n\n### Créer un service account\n\nPour un reporting automatisé (cron, CI/CD, serverless), le service account est la seule option viable. L'OAuth2 interactif avec refresh token finit toujours par casser en production — tokens expirés, révocations silencieuses.\n\n```bash\n# Créer le service account via gcloud CLI\ngcloud iam service-accounts create gsc-reporting \\\n  --display-name=\"GSC Reporting Bot\" \\\n  --project=mon-projet-seo\n\n# Générer la clé JSON\ngcloud iam service-accounts keys create gsc-credentials.json \\\n  --iam-account=gsc-reporting@mon-projet-seo.iam.gserviceaccount.com\n\n# Activer l'API Search Console sur le projet\ngcloud services enable searchconsole.googleapis.com \\\n  --project=mon-projet-seo\n```\n\nÉtape critique : ajoutez l'email du service account (`gsc-reporting@mon-projet-seo.iam.gserviceaccount.com`) comme **utilisateur avec droits complets** dans la Search Console, section \"Paramètres > Utilisateurs et autorisations\". Sans cela, l'API retourne un 403 sans message explicite.\n\n### Script d'authentification Python\n\nLa librairie `google-auth` gère le cycle de vie des tokens automatiquement. Évitez `oauth2client`, déprécié depuis 2020.\n\n```python\nfrom google.oauth2 import service_account\nfrom googleapiclient.discovery import build\n\nSCOPES = ['https://www.googleapis.com/auth/webmasters.readonly']\nCREDENTIALS_FILE = 'gsc-credentials.json'\n\ndef get_gsc_service():\n    credentials = service_account.Credentials.from_service_account_file(\n        CREDENTIALS_FILE, scopes=SCOPES\n    )\n    return build('searchconsole', 'v1', credentials=credentials)\n\nservice = get_gsc_service()\n\n# Vérifier l'accès : lister les propriétés disponibles\nsites = service.sites().list().execute()\nfor site in sites.get('siteEntry', []):\n    print(f\"{site['siteUrl']} — {site['permissionLevel']}\")\n```\n\nSi vous gérez une propriété de domaine (format `sc-domain:example.com`), utilisez cette syntaxe exacte comme `siteUrl` dans vos appels. Le format `https://example.com/` correspond à une propriété URL prefix — les données diffèrent.\n\n## Construire des requêtes analytiques avancées\n\nL'endpoint `searchAnalytics.query` accepte un body JSON avec dimensions, filtres, et paramètres de pagination. C'est ici que la puissance de l'API se révèle par rapport à l'interface.\n\n### Extraire toutes les données sans troncature\n\nLe problème des 25 000 lignes se résout par segmentation. Plutôt que de demander `query × page` pour tout le site, segmentez par répertoire, par device, ou par plage de dates.\n\n```python\nimport datetime\nimport time\nimport pandas as pd\n\ndef fetch_gsc_data(service, site_url, start_date, end_date, dimensions, row_limit=25000):\n    \"\"\"\n    Extraction paginée avec gestion du seuil de 25K lignes.\n    L'API ne supporte pas la pagination native — on segmente par date.\n    \"\"\"\n    all_rows = []\n    current_date = start_date\n\n    while current_date \u003C= end_date:\n        request_body = {\n            'startDate': current_date.isoformat(),\n            'endDate': current_date.isoformat(),\n            'dimensions': dimensions,\n            'rowLimit': row_limit,\n            'dataState': 'final'  # Exclut les données incomplètes\n        }\n\n        response = service.searchanalytics().query(\n            siteUrl=site_url,\n            body=request_body\n        ).execute()\n\n        rows = response.get('rows', [])\n        for row in rows:\n            record = {}\n            for i, dim in enumerate(dimensions):\n                record[dim] = row['keys'][i]\n            record['clicks'] = row['clicks']\n            record['impressions'] = row['impressions']\n            record['ctr'] = row['ctr']\n            record['position'] = row['position']\n            record['date'] = current_date.isoformat()\n            all_rows.append(record)\n\n        # Détection de troncature\n        if len(rows) == row_limit:\n            print(f\"⚠ Troncature probable le {current_date} — {len(rows)} lignes\")\n\n        current_date += datetime.timedelta(days=1)\n        time.sleep(0.1)  # Respecter le rate limit\n\n    return pd.DataFrame(all_rows)\n\n\n# Usage : extraction query × page sur 30 jours\nsite = 'sc-domain:monsite-ecommerce.fr'\nstart = datetime.date(2026, 3, 1)\nend = datetime.date(2026, 3, 31)\n\ndf = fetch_gsc_data(\n    service=get_gsc_service(),\n    site_url=site,\n    start_date=start,\n    end_date=end,\n    dimensions=['query', 'page']\n)\n\nprint(f\"Total : {len(df)} lignes, {df['clicks'].sum()} clics\")\n```\n\nLe paramètre `dataState: 'final'` mérite attention. Par défaut, l'API retourne les données \"all\" (incluant les estimations fraîches). En forçant \"final\", vous obtenez des chiffres stabilisés mais perdez les 2-3 derniers jours. Pour un reporting hebdomadaire, c'est le bon choix. Pour du monitoring temps réel, gardez \"all\" et acceptez les fluctuations.\n\n### Filtres avancés : isoler les segments qui comptent\n\nL'API supporte des filtres sur chaque dimension avec les opérateurs `contains`, `equals`, `notContains`, `notEquals`, et `includingRegex` / `excludingRegex`. Les regex sont des RE2, pas du PCRE — pas de lookahead ni de backreference.\n\n```python\n# Isoler les performances des pages catégories d'un e-commerce\nrequest_body = {\n    'startDate': '2026-03-01',\n    'endDate': '2026-03-31',\n    'dimensions': ['page', 'query'],\n    'dimensionFilterGroups': [{\n        'filters': [\n            {\n                'dimension': 'page',\n                'operator': 'includingRegex',\n                'expression': '/categorie/[a-z-]+/$'\n            },\n            {\n                'dimension': 'query',\n                'operator': 'excludingRegex',\n                'expression': 'marque|brand|monsite'\n            }\n        ]\n    }],\n    'rowLimit': 25000,\n    'dataState': 'final'\n}\n```\n\nCe type de filtre est exactement ce que l'interface ne permet pas facilement. Ici, on isole les requêtes non-branded sur les pages catégories — le segment qui mesure réellement la performance SEO organique, particulièrement pertinent pour [optimiser vos pages catégories e-commerce](/blog/seo-pour-les-pages-categories-e-commerce).\n\n## Scénario concret : monitoring de migration SSR\n\nContexte réel : un site média de 8 000 articles migre d'une SPA Angular vers Next.js avec SSR. L'équipe SEO a besoin de mesurer l'impact de la migration semaine par semaine, page par page.\n\n### Le problème\n\nAprès le déploiement progressif (20 % des URLs la première semaine, 100 % en semaine 4), l'interface Search Console montre une courbe agrégée. Impossible de distinguer les pages déjà migrées des pages encore en SPA. Impossible de détecter qu'un template spécifique (les pages \"auteur\", 450 URLs) a un bug SSR qui retourne un `\u003Ctitle>` vide.\n\n### La solution automatisée\n\nUn script Python tourne chaque lundi à 6h via un cron sur un serveur interne. Il compare les métriques de chaque URL entre la semaine N et N-1, filtrées par un fichier CSV des URLs migrées chaque semaine.\n\n```python\nimport pandas as pd\n\ndef migration_impact_report(service, site_url, migrated_urls_csv, week_start, week_end, prev_week_start, prev_week_end):\n    \"\"\"\n    Compare les performances pre/post migration pour un batch d'URLs.\n    \"\"\"\n    migrated = pd.read_csv(migrated_urls_csv)  # colonnes: url, migration_date\n    \n    # Données semaine courante\n    df_current = fetch_gsc_data(\n        service, site_url,\n        start_date=week_start,\n        end_date=week_end,\n        dimensions=['page', 'device']\n    )\n    \n    # Données semaine précédente\n    df_previous = fetch_gsc_data(\n        service, site_url,\n        start_date=prev_week_start,\n        end_date=prev_week_end,\n        dimensions=['page', 'device']\n    )\n    \n    # Agréger par page\n    current_agg = df_current.groupby('page').agg(\n        clicks=('clicks', 'sum'),\n        impressions=('impressions', 'sum'),\n        avg_position=('position', 'mean')\n    ).reset_index()\n    \n    previous_agg = df_previous.groupby('page').agg(\n        clicks_prev=('clicks', 'sum'),\n        impressions_prev=('impressions', 'sum'),\n        avg_position_prev=('position', 'mean')\n    ).reset_index()\n    \n    # Merge et calcul des deltas\n    merged = current_agg.merge(previous_agg, on='page', how='outer')\n    merged = merged.merge(migrated[['url']], left_on='page', right_on='url', how='inner')\n    \n    merged['clicks_delta_pct'] = (\n        (merged['clicks'] - merged['clicks_prev']) / merged['clicks_prev'] * 100\n    ).round(1)\n    \n    merged['position_delta'] = (\n        merged['avg_position'] - merged['avg_position_prev']\n    ).round(2)\n    \n    # Alertes : pages avec chute > 30% de clics\n    regressions = merged[merged['clicks_delta_pct'] \u003C -30].sort_values('clicks_delta_pct')\n    \n    if not regressions.empty:\n        print(f\"🚨 {len(regressions)} pages avec régression > 30% :\")\n        for _, row in regressions.head(20).iterrows():\n            print(f\"  {row['page']}: {row['clicks_delta_pct']}% clics, \"\n                  f\"position {row['avg_position_prev']:.1f} → {row['avg_position']:.1f}\")\n    \n    return merged\n```\n\n### Résultats observés\n\nSur les 450 pages auteur, le script a détecté une chute de 45 % des impressions dès la semaine 2 de la migration. Investigation : le composant React `\u003CAuthorHead>` ne s'exécutait pas côté serveur, générant un `\u003Ctitle>` par défaut \"Loading...\" dans le HTML rendu par le serveur. Googlebot indexait ce titre. L'interface Search Console montrait une baisse globale de 3 % — noyée dans le bruit. Le script, lui, isolait précisément le répertoire `/auteur/` comme anomalie.\n\nCe type de régression silencieuse est exactement ce qui se produit lors de [déploiements mal supervisés](/blog/deploiement-vendredi-soir-comment-eviter-la-catastrophe-seo) et fait partie des [régressions SEO les plus fréquentes](/blog/regressions-seo-les-10-types-les-plus-frequents) en environnement JavaScript. La détection via l'API Search Console intervient à J+3 au mieux — un outil de monitoring comme Seogard détecte ce type de divergence SSR/CSR en temps réel, avant même que Google n'indexe la page cassée.\n\n## Rapports personnalisés : aller au-delà des dashboards standards\n\nLes dashboards Looker Studio (ex Data Studio) connectés à la Search Console souffrent des mêmes limitations que l'interface : 1 000 lignes, pas de filtres regex, pas de croisement avancé. L'API vous permet de construire des rapports qu'aucun outil standard ne propose.\n\n### Rapport de cannibalisation de mots-clés\n\nLa cannibalisation — deux pages qui se disputent la même requête — se détecte quand une requête génère des impressions sur plusieurs URLs avec des positions qui oscillent. L'interface ne permet pas cette analyse. L'API, si.\n\n```python\ndef detect_cannibalization(df, min_impressions=50, min_pages=2):\n    \"\"\"\n    Identifie les requêtes pour lesquelles plusieurs pages se positionnent,\n    signe potentiel de cannibalisation.\n    \n    Input: DataFrame avec colonnes 'query', 'page', 'clicks', 'impressions', 'position'\n    \"\"\"\n    # Agréger par query + page sur la période\n    agg = df.groupby(['query', 'page']).agg(\n        total_clicks=('clicks', 'sum'),\n        total_impressions=('impressions', 'sum'),\n        avg_position=('position', 'mean'),\n        position_std=('position', 'std')  # Volatilité de position\n    ).reset_index()\n    \n    # Filtrer les requêtes avec assez d'impressions\n    agg = agg[agg['total_impressions'] >= min_impressions]\n    \n    # Compter le nombre de pages par requête\n    pages_per_query = agg.groupby('query')['page'].nunique().reset_index()\n    pages_per_query.columns = ['query', 'page_count']\n    \n    cannibalized = pages_per_query[pages_per_query['page_count'] >= min_pages]\n    \n    # Enrichir avec les détails\n    result = agg.merge(cannibalized[['query']], on='query')\n    result = result.sort_values(['query', 'total_impressions'], ascending=[True, False])\n    \n    # Score de cannibalisation : forte si les positions sont proches\n    # et les impressions réparties entre les pages\n    for query in result['query'].unique():\n        query_data = result[result['query'] == query]\n        if len(query_data) >= 2:\n            top_two = query_data.head(2)\n            position_gap = abs(top_two.iloc[0]['avg_position'] - top_two.iloc[1]['avg_position'])\n            impression_ratio = top_two.iloc[1]['total_impressions'] / top_two.iloc[0]['total_impressions']\n            \n            # Cannibalisation sévère : positions proches ET impressions réparties\n            if position_gap \u003C 5 and impression_ratio > 0.3:\n                result.loc[result['query'] == query, 'severity'] = 'high'\n            else:\n                result.loc[result['query'] == query, 'severity'] = 'low'\n    \n    return result\n\n# Exécution\ncannib = detect_cannibalization(df, min_impressions=100)\nhigh_severity = cannib[cannib['severity'] == 'high']\nprint(f\"{high_severity['query'].nunique()} requêtes avec cannibalisation sévère\")\n```\n\nCe rapport, exécuté mensuellement, alimente directement la stratégie de [maillage interne](/blog/internal-linking-pour-l-e-commerce-strategies-avancees) : les pages cannibalisées sont candidates à une consolidation (redirect 301 + fusion de contenu) ou à une différenciation intentionnelle.\n\n### Rapport de contenu \"zombie\"\n\nCroisez les données GSC avec votre crawl Screaming Frog pour identifier les pages indexées qui ne génèrent ni clic ni impression — un travail impossible manuellement au-delà de quelques centaines de pages.\n\n```python\ndef find_zombie_pages(gsc_df, crawl_export_csv, min_days=90):\n    \"\"\"\n    Croise les données GSC (16 mois) avec l'export Screaming Frog\n    pour trouver les pages indexables sans aucune visibilité.\n    \"\"\"\n    crawl = pd.read_csv(crawl_export_csv)\n    \n    # Pages indexables selon le crawl\n    indexable = crawl[\n        (crawl['Status Code'] == 200) &\n        (crawl['Indexability'] == 'Indexable')\n    ]['Address'].tolist()\n    \n    # Pages ayant au moins 1 impression sur la période\n    visible = set(gsc_df[gsc_df['impressions'] > 0]['page'].unique())\n    \n    # Zombies = indexables sans aucune impression\n    zombies = [url for url in indexable if url not in visible]\n    \n    print(f\"{len(zombies)} pages zombies sur {len(indexable)} indexables \"\n          f\"({len(zombies)/len(indexable)*100:.1f}%)\")\n    \n    return zombies\n```\n\nSur un e-commerce de 15 000 pages, ce script révèle typiquement 20 à 35 % de pages zombies — souvent des pages produit en rupture de stock, des variations de filtres mal canonicalisées, ou du [contenu thin](/blog/thin-content-quand-vos-pages-nuisent-au-seo-global) généré par des combinaisons de facettes. Ces pages grignotent le crawl budget sans aucun retour.\n\n## Automatiser l'exécution et la distribution\n\nUn script qui tourne manuellement sur votre machine n'est pas de l'automatisation. Pour qu'un reporting soit réellement utile, il doit tourner seul, alerter quand les seuils sont franchis, et distribuer les résultats aux bonnes personnes.\n\n### Orchestration avec GitHub Actions\n\nPour les équipes qui n'ont pas d'infrastructure cron dédiée, GitHub Actions offre un runner gratuit (2 000 minutes/mois sur les repos privés) avec la gestion native des secrets pour les credentials.\n\n```yaml\n# .github/workflows/gsc-reporting.yml\nname: Weekly GSC Report\n\non:\n  schedule:\n    - cron: '0 6 * * 1'  # Chaque lundi à 6h UTC\n  workflow_dispatch:  # Permet l'exécution manuelle\n\njobs:\n  generate-report:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.12'\n\n      - name: Install dependencies\n        run: pip install google-api-python-client google-auth pandas\n\n      - name: Write credentials\n        run: echo '${{ secrets.GSC_CREDENTIALS_JSON }}' > gsc-credentials.json\n\n      - name: Run report\n        run: python scripts/weekly_gsc_report.py\n        env:\n          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}\n          SITE_URL: 'sc-domain:monsite-ecommerce.fr'\n\n      - name: Upload report artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: gsc-report-${{ github.run_number }}\n          path: reports/\n          retention-days: 90\n```\n\nL'intégration dans la CI/CD n'est pas un luxe : c'est la suite logique de la démarche d'[automatisation des checks SEO](/blog/automatiser-les-checks-seo-dans-le-ci-cd) que toute équipe technique devrait avoir en place.\n\n### Alerting Slack avec seuils configurables\n\nLa partie alerting est ce qui transforme un reporting passif en système de surveillance actif. Les [seuils d'alerte](/blog/alertes-seo-quels-seuils-et-quelle-frequence) doivent être calibrés au contexte du site — un site média avec une forte saisonnalité n'aura pas les mêmes seuils qu'un SaaS B2B.\n\n```python\nimport json\nimport urllib.request\n\ndef send_slack_alert(webhook_url, regressions_df, threshold_pct=-20):\n    \"\"\"\n    Envoie une alerte Slack si des pages franchissent le seuil de régression.\n    \"\"\"\n    critical = regressions_df[regressions_df['clicks_delta_pct'] \u003C threshold_pct]\n    \n    if critical.empty:\n        return\n    \n    total_lost_clicks = int(\n        critical['clicks_prev'].sum() - critical['clicks'].sum()\n    )\n    \n    blocks = [\n        {\n            \"type\": \"header\",\n            \"text\": {\n                \"type\": \"plain_text\",\n                \"text\": f\"🚨 {len(critical)} pages en régression SEO\"\n            }\n        },\n        {\n            \"type\": \"section\",\n            \"text\": {\n                \"type\": \"mrkdwn\",\n                \"text\": f\"*{total_lost_clicks} clics perdus* cette semaine vs semaine précédente.\\n\"\n                        f\"Seuil d'alerte : {threshold_pct}%\"\n            }\n        }\n    ]\n    \n    # Top 5 des pires régressions\n    for _, row in critical.head(5).iterrows():\n        blocks.append({\n            \"type\": \"section\",\n            \"text\": {\n                \"type\": \"mrkdwn\",\n                \"text\": f\"• `{row['page']}`\\n\"\n                        f\"  Clics : {int(row['clicks_prev'])} → {int(row['clicks'])} \"\n                        f\"({row['clicks_delta_pct']}%) | \"\n                        f\"Position : {row['avg_position_prev']:.1f} → {row['avg_position']:.1f}\"\n            }\n        })\n    \n    payload = json.dumps({\"blocks\": blocks}).encode('utf-8')\n    req = urllib.request.Request(\n        webhook_url,\n        data=payload,\n        headers={'Content-Type': 'application/json'}\n    )\n    urllib.request.urlopen(req)\n```\n\n## Aller plus loin : combiner l'API GSC avec d'autres sources\n\nL'API Search Console seule donne la visibilité organique. Combinée à d'autres sources de données, elle devient un véritable système de diagnostic.\n\n### Croisement avec les logs serveur\n\nL'API vous dit combien Google affiche et clique vos pages. Les logs serveur vous disent combien Googlebot les crawle. Le croisement révèle trois catégories critiques :\n\n- **Crawlé mais invisible** : Googlebot visite la page régulièrement, mais elle n'apparaît jamais dans les résultats. Problème d'indexabilité (noindex non détecté, canonical vers une autre page, contenu dupliqué).\n- **Visible mais non crawlé récemment** : la page génère des impressions, mais le dernier crawl remonte à 30+ jours. Risque de désindexation progressive.\n- **Ni crawlé ni visible** : page orpheline ou enterrée dans l'architecture. Le [choix de structure flat vs deep](/blog/architecture-de-site-seo-flat-vs-deep-structure) influence directement ce ratio.\n\n### Croisement avec l'API URL Inspection\n\nL'API URL Inspection (disponible depuis 2022) permet de vérifier programmatiquement le statut d'indexation de chaque URL. Limitée à 2 000 requêtes/jour/propriété, elle est trop lente pour un crawl complet mais parfaite pour vérifier les URLs identifiées comme problématiques par votre rapport GSC.\n\n```python\ndef check_indexation_status(service, site_url, urls):\n    \"\"\"\n    Vérifie le statut d'indexation via l'API URL Inspection.\n    Attention : 2000 requêtes/jour max par propriété.\n    \"\"\"\n    results = []\n    for url in urls[:2000]:  # Hard limit\n        try:\n            response = service.urlInspection().index().inspect({\n                'inspectionUrl': url,\n                'siteUrl': site_url\n            }).execute()\n            \n            result = response.get('inspectionResult', {})\n            index_status = result.get('indexStatusResult', {})\n            \n            results.append({\n                'url': url,\n                'verdict': index_status.get('verdict', 'UNKNOWN'),\n                'coverage_state': index_status.get('coverageState', ''),\n                'robots_txt_state': index_status.get('robotsTxtState', ''),\n                'last_crawl_time': index_status.get('lastCrawlTime', ''),\n                'page_fetch_state': index_status.get('pageFetchState', '')\n            })\n            \n            time.sleep(0.5)  # Respecter le quota\n            \n        except Exception as e:\n            results.append({'url': url, 'verdict': 'ERROR', 'error': str(e)})\n    \n    return pd.DataFrame(results)\n```\n\nCe croisement vous donne une image complète : performance de recherche (GSC Analytics API) + statut d'indexation (URL Inspection API) + comportement de crawl (logs). C'est la base d'un [monitoring SEO continu et actionnable](/blog/monitoring-seo-pourquoi-les-audits-ponctuels-ne-suffisent-plus), bien au-delà de ce que permettent les rapports ponctuels.\n\nLes données brutes de la Search Console, enfin exploitées sans limite de lignes ni de dimensions, deviennent le socle d'un système de surveillance SEO qui détecte les problèmes avant qu'ils n'impactent le trafic. Pour les régressions qui nécessitent une détection en temps réel — un SSR cassé, un canonical qui disparaît après un déploiement — combiner ces rapports API avec un monitoring côté crawl comme Seogard couvre l'intégralité du spectre, du diagnostic post-hoc à l'alerte instantanée.\n```","https://seogard.io/blog/search-console-api-automatiser-le-reporting-seo","Analytics","2026-04-08T18:02:16.042Z","2026-04-08","Exploitez l'API Google Search Console pour créer des rapports SEO personnalisés, automatisés et actionnables. Guide technique avec code Python.","\u003Cp>L'interface web de Google Search Console plafonne à 1 000 lignes exportables. Pour un site e-commerce de 20 000 pages produit, ça revient à analyser 5 % de vos données de performance. L'API Search Console supprime cette limite et ouvre la porte à des rapports que l'interface ne permettra jamais : croisement requêtes × pages × devices sur 16 mois, détection automatique de chutes de CTR, alerting sur les régressions de positionnement.\u003C/p>\n\u003Ch2>Comprendre les limites de l'API (et les contourner)\u003C/h2>\n\u003Cp>L'API Search Console (officiellement \"Search Analytics API\", intégrée à l'API Google Search Console v1) n'est pas un miroir parfait de l'interface. Plusieurs contraintes techniques conditionnent la façon dont vous allez structurer vos appels.\u003C/p>\n\u003Ch3>Quotas et rate limiting\u003C/h3>\n\u003Cp>Google impose un quota de \u003Cstrong>1 200 requêtes par minute par projet\u003C/strong> et \u003Cstrong>50 000 requêtes par jour\u003C/strong>. Pour la majorité des sites, c'est largement suffisant. Le problème survient quand vous interrogez plusieurs propriétés (multi-sites, versions www/non-www, domaines internationaux) depuis le même projet GCP.\u003C/p>\n\u003Cp>Chaque appel à \u003Ccode>searchAnalytics.query\u003C/code> retourne au maximum \u003Cstrong>25 000 lignes\u003C/strong>. Si votre combinaison dimensions/filtres dépasse ce seuil, les données sont tronquées silencieusement — pas d'erreur, pas de warning. C'est le piège classique.\u003C/p>\n\u003Ch3>La fenêtre de données\u003C/h3>\n\u003Cp>L'API expose les données des \u003Cstrong>16 derniers mois\u003C/strong>, avec un délai de 2 à 3 jours. Les données des 48 dernières heures sont instables — Google les recalcule. Planifier vos pulls à J-3 minimum évite les écarts entre deux extractions successives.\u003C/p>\n\u003Ch3>Dimensions disponibles\u003C/h3>\n\u003Cp>Cinq dimensions sont combinables : \u003Ccode>query\u003C/code>, \u003Ccode>page\u003C/code>, \u003Ccode>country\u003C/code>, \u003Ccode>device\u003C/code>, \u003Ccode>searchAppearance\u003C/code>. Vous pouvez en combiner jusqu'à trois dans un seul appel. Au-delà, l'API refuse la requête. Ce qui signifie que pour obtenir une vue \u003Ccode>query × page × device × country\u003C/code>, il faut deux appels distincts et un join côté client.\u003C/p>\n\u003Cp>Un point souvent ignoré dans la documentation : la dimension \u003Ccode>searchAppearance\u003C/code> est \u003Cstrong>mutuellement exclusive\u003C/strong> avec les autres combinaisons de trois dimensions. Si vous l'utilisez, limitez-vous à deux dimensions supplémentaires.\u003C/p>\n\u003Ch2>Configurer l'authentification OAuth2\u003C/h2>\n\u003Cp>Avant toute chose, vous avez besoin d'un projet Google Cloud Platform avec l'API Search Console activée. La documentation officielle détaille la procédure : \u003Ca href=\"https://developers.google.com/webmaster-tools/v1/how-tos/authorizing\">Google Search Console API - Getting Started\u003C/a>.\u003C/p>\n\u003Ch3>Créer un service account\u003C/h3>\n\u003Cp>Pour un reporting automatisé (cron, CI/CD, serverless), le service account est la seule option viable. L'OAuth2 interactif avec refresh token finit toujours par casser en production — tokens expirés, révocations silencieuses.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Créer le service account via gcloud CLI\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">gcloud\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> iam\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> service-accounts\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> create\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> gsc-reporting\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  --display-name=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"GSC Reporting Bot\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  --project=mon-projet-seo\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Générer la clé JSON\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">gcloud\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> iam\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> service-accounts\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> keys\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> create\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> gsc-credentials.json\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  --iam-account=gsc-reporting@mon-projet-seo.iam.gserviceaccount.com\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Activer l'API Search Console sur le projet\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">gcloud\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> services\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> enable\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> searchconsole.googleapis.com\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  --project=mon-projet-seo\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Étape critique : ajoutez l'email du service account (\u003Ccode>gsc-reporting@mon-projet-seo.iam.gserviceaccount.com\u003C/code>) comme \u003Cstrong>utilisateur avec droits complets\u003C/strong> dans la Search Console, section \"Paramètres > Utilisateurs et autorisations\". Sans cela, l'API retourne un 403 sans message explicite.\u003C/p>\n\u003Ch3>Script d'authentification Python\u003C/h3>\n\u003Cp>La librairie \u003Ccode>google-auth\u003C/code> gère le cycle de vie des tokens automatiquement. Évitez \u003Ccode>oauth2client\u003C/code>, déprécié depuis 2020.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> google.oauth2 \u003C/span>\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> service_account\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> googleapiclient.discovery \u003C/span>\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> build\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">SCOPES\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'https://www.googleapis.com/auth/webmasters.readonly'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">CREDENTIALS_FILE\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'gsc-credentials.json'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">def\u003C/span>\u003Cspan style=\"color:#B392F0\"> get_gsc_service\u003C/span>\u003Cspan style=\"color:#E1E4E8\">():\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    credentials \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> service_account.Credentials.from_service_account_file(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">        CREDENTIALS_FILE\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">scopes\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">SCOPES\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    )\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> build(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'searchconsole'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'v1'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">credentials\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">credentials)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">service \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> get_gsc_service()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Vérifier l'accès : lister les propriétés disponibles\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">sites \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> service.sites().list().execute()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> site \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> sites.get(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'siteEntry'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, []):\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    print\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#F97583\">f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#79B8FF\">{\u003C/span>\u003Cspan style=\"color:#E1E4E8\">site[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'siteUrl'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> — \u003C/span>\u003Cspan style=\"color:#79B8FF\">{\u003C/span>\u003Cspan style=\"color:#E1E4E8\">site[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'permissionLevel'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Si vous gérez une propriété de domaine (format \u003Ccode>sc-domain:example.com\u003C/code>), utilisez cette syntaxe exacte comme \u003Ccode>siteUrl\u003C/code> dans vos appels. Le format \u003Ccode>https://example.com/\u003C/code> correspond à une propriété URL prefix — les données diffèrent.\u003C/p>\n\u003Ch2>Construire des requêtes analytiques avancées\u003C/h2>\n\u003Cp>L'endpoint \u003Ccode>searchAnalytics.query\u003C/code> accepte un body JSON avec dimensions, filtres, et paramètres de pagination. C'est ici que la puissance de l'API se révèle par rapport à l'interface.\u003C/p>\n\u003Ch3>Extraire toutes les données sans troncature\u003C/h3>\n\u003Cp>Le problème des 25 000 lignes se résout par segmentation. Plutôt que de demander \u003Ccode>query × page\u003C/code> pour tout le site, segmentez par répertoire, par device, ou par plage de dates.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> datetime\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> time\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> pandas \u003C/span>\u003Cspan style=\"color:#F97583\">as\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> pd\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">def\u003C/span>\u003Cspan style=\"color:#B392F0\"> fetch_gsc_data\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(service, site_url, start_date, end_date, dimensions, row_limit\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">25000\u003C/span>\u003Cspan style=\"color:#E1E4E8\">):\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    Extraction paginée avec gestion du seuil de 25K lignes.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    L'API ne supporte pas la pagination native — on segmente par date.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    all_rows \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> []\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    current_date \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> start_date\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    while\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> current_date \u003C/span>\u003Cspan style=\"color:#F97583\">&#x3C;=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> end_date:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        request_body \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            'startDate'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: current_date.isoformat(),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            'endDate'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: current_date.isoformat(),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            'dimensions'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: dimensions,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            'rowLimit'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: row_limit,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            'dataState'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'final'\u003C/span>\u003Cspan style=\"color:#6A737D\">  # Exclut les données incomplètes\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        response \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> service.searchanalytics().query(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">            siteUrl\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">site_url,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">            body\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">request_body\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        ).execute()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        rows \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> response.get(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'rows'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, [])\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> row \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> rows:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            record \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">            for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> i, dim \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#79B8FF\"> enumerate\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(dimensions):\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">                record[dim] \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> row[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'keys'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">][i]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            record[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> row[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            record[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'impressions'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> row[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'impressions'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            record[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'ctr'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> row[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'ctr'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            record[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'position'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> row[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'position'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            record[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'date'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> current_date.isoformat()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            all_rows.append(record)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">        # Détection de troncature\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        if\u003C/span>\u003Cspan style=\"color:#79B8FF\"> len\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(rows) \u003C/span>\u003Cspan style=\"color:#F97583\">==\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> row_limit:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">            print\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#F97583\">f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"⚠ Troncature probable le \u003C/span>\u003Cspan style=\"color:#79B8FF\">{\u003C/span>\u003Cspan style=\"color:#E1E4E8\">current_date\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> — \u003C/span>\u003Cspan style=\"color:#79B8FF\">{len\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(rows)\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> lignes\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        current_date \u003C/span>\u003Cspan style=\"color:#F97583\">+=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> datetime.timedelta(\u003C/span>\u003Cspan style=\"color:#FFAB70\">days\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        time.sleep(\u003C/span>\u003Cspan style=\"color:#79B8FF\">0.1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)  \u003C/span>\u003Cspan style=\"color:#6A737D\"># Respecter le rate limit\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> pd.DataFrame(all_rows)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Usage : extraction query × page sur 30 jours\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">site \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'sc-domain:monsite-ecommerce.fr'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">start \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> datetime.date(\u003C/span>\u003Cspan style=\"color:#79B8FF\">2026\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">3\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">end \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> datetime.date(\u003C/span>\u003Cspan style=\"color:#79B8FF\">2026\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">3\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">31\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">df \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> fetch_gsc_data(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">    service\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">get_gsc_service(),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">    site_url\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">site,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">    start_date\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">start,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">    end_date\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">end,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">    dimensions\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'query'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">print\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#F97583\">f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"Total : \u003C/span>\u003Cspan style=\"color:#79B8FF\">{len\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(df)\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> lignes, \u003C/span>\u003Cspan style=\"color:#79B8FF\">{\u003C/span>\u003Cspan style=\"color:#E1E4E8\">df[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].sum()\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> clics\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le paramètre \u003Ccode>dataState: 'final'\u003C/code> mérite attention. Par défaut, l'API retourne les données \"all\" (incluant les estimations fraîches). En forçant \"final\", vous obtenez des chiffres stabilisés mais perdez les 2-3 derniers jours. Pour un reporting hebdomadaire, c'est le bon choix. Pour du monitoring temps réel, gardez \"all\" et acceptez les fluctuations.\u003C/p>\n\u003Ch3>Filtres avancés : isoler les segments qui comptent\u003C/h3>\n\u003Cp>L'API supporte des filtres sur chaque dimension avec les opérateurs \u003Ccode>contains\u003C/code>, \u003Ccode>equals\u003C/code>, \u003Ccode>notContains\u003C/code>, \u003Ccode>notEquals\u003C/code>, et \u003Ccode>includingRegex\u003C/code> / \u003Ccode>excludingRegex\u003C/code>. Les regex sont des RE2, pas du PCRE — pas de lookahead ni de backreference.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Isoler les performances des pages catégories d'un e-commerce\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">request_body \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'startDate'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'2026-03-01'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'endDate'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'2026-03-31'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'dimensions'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: [\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'query'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">],\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'dimensionFilterGroups'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: [{\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        'filters'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                'dimension'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                'operator'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'includingRegex'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                'expression'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/categorie/[a-z-]+/$'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                'dimension'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'query'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                'operator'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'excludingRegex'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                'expression'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'marque|brand|monsite'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        ]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }],\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'rowLimit'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">25000\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'dataState'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'final'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Ce type de filtre est exactement ce que l'interface ne permet pas facilement. Ici, on isole les requêtes non-branded sur les pages catégories — le segment qui mesure réellement la performance SEO organique, particulièrement pertinent pour \u003Ca href=\"/blog/seo-pour-les-pages-categories-e-commerce\">optimiser vos pages catégories e-commerce\u003C/a>.\u003C/p>\n\u003Ch2>Scénario concret : monitoring de migration SSR\u003C/h2>\n\u003Cp>Contexte réel : un site média de 8 000 articles migre d'une SPA Angular vers Next.js avec SSR. L'équipe SEO a besoin de mesurer l'impact de la migration semaine par semaine, page par page.\u003C/p>\n\u003Ch3>Le problème\u003C/h3>\n\u003Cp>Après le déploiement progressif (20 % des URLs la première semaine, 100 % en semaine 4), l'interface Search Console montre une courbe agrégée. Impossible de distinguer les pages déjà migrées des pages encore en SPA. Impossible de détecter qu'un template spécifique (les pages \"auteur\", 450 URLs) a un bug SSR qui retourne un \u003Ccode>&#x3C;title>\u003C/code> vide.\u003C/p>\n\u003Ch3>La solution automatisée\u003C/h3>\n\u003Cp>Un script Python tourne chaque lundi à 6h via un cron sur un serveur interne. Il compare les métriques de chaque URL entre la semaine N et N-1, filtrées par un fichier CSV des URLs migrées chaque semaine.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> pandas \u003C/span>\u003Cspan style=\"color:#F97583\">as\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> pd\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">def\u003C/span>\u003Cspan style=\"color:#B392F0\"> migration_impact_report\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(service, site_url, migrated_urls_csv, week_start, week_end, prev_week_start, prev_week_end):\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    Compare les performances pre/post migration pour un batch d'URLs.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    migrated \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> pd.read_csv(migrated_urls_csv)  \u003C/span>\u003Cspan style=\"color:#6A737D\"># colonnes: url, migration_date\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Données semaine courante\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    df_current \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> fetch_gsc_data(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        service, site_url,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        start_date\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">week_start,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        end_date\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">week_end,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        dimensions\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'device'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    )\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Données semaine précédente\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    df_previous \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> fetch_gsc_data(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        service, site_url,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        start_date\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">prev_week_start,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        end_date\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">prev_week_end,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        dimensions\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'device'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    )\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Agréger par page\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    current_agg \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> df_current.groupby(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">).agg(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        clicks\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'sum'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        impressions\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'impressions'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'sum'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        avg_position\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'position'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'mean'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ).reset_index()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    previous_agg \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> df_previous.groupby(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">).agg(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        clicks_prev\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'sum'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        impressions_prev\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'impressions'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'sum'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        avg_position_prev\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'position'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'mean'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ).reset_index()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Merge et calcul des deltas\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    merged \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> current_agg.merge(previous_agg, \u003C/span>\u003Cspan style=\"color:#FFAB70\">on\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">how\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'outer'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    merged \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> merged.merge(migrated[[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'url'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]], \u003C/span>\u003Cspan style=\"color:#FFAB70\">left_on\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">right_on\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'url'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">how\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'inner'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    merged[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks_delta_pct'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        (merged[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">-\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> merged[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks_prev'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]) \u003C/span>\u003Cspan style=\"color:#F97583\">/\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> merged[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks_prev'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">*\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 100\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ).round(\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    merged[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'position_delta'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        merged[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'avg_position'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">-\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> merged[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'avg_position_prev'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ).round(\u003C/span>\u003Cspan style=\"color:#79B8FF\">2\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Alertes : pages avec chute > 30% de clics\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    regressions \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> merged[merged[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks_delta_pct'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">&#x3C;\u003C/span>\u003Cspan style=\"color:#F97583\"> -\u003C/span>\u003Cspan style=\"color:#79B8FF\">30\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].sort_values(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks_delta_pct'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#F97583\"> not\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> regressions.empty:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">        print\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#F97583\">f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"🚨 \u003C/span>\u003Cspan style=\"color:#79B8FF\">{len\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(regressions)\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> pages avec régression > 30% :\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> _, row \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> regressions.head(\u003C/span>\u003Cspan style=\"color:#79B8FF\">20\u003C/span>\u003Cspan style=\"color:#E1E4E8\">).iterrows():\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">            print\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#F97583\">f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"  \u003C/span>\u003Cspan style=\"color:#79B8FF\">{\u003C/span>\u003Cspan style=\"color:#E1E4E8\">row[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">{\u003C/span>\u003Cspan style=\"color:#E1E4E8\">row[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks_delta_pct'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\">% clics, \"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">                  f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"position \u003C/span>\u003Cspan style=\"color:#79B8FF\">{\u003C/span>\u003Cspan style=\"color:#E1E4E8\">row[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'avg_position_prev'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003Cspan style=\"color:#F97583\">:.1f\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> → \u003C/span>\u003Cspan style=\"color:#79B8FF\">{\u003C/span>\u003Cspan style=\"color:#E1E4E8\">row[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'avg_position'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003Cspan style=\"color:#F97583\">:.1f\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> merged\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch3>Résultats observés\u003C/h3>\n\u003Cp>Sur les 450 pages auteur, le script a détecté une chute de 45 % des impressions dès la semaine 2 de la migration. Investigation : le composant React \u003Ccode>&#x3C;AuthorHead>\u003C/code> ne s'exécutait pas côté serveur, générant un \u003Ccode>&#x3C;title>\u003C/code> par défaut \"Loading...\" dans le HTML rendu par le serveur. Googlebot indexait ce titre. L'interface Search Console montrait une baisse globale de 3 % — noyée dans le bruit. Le script, lui, isolait précisément le répertoire \u003Ccode>/auteur/\u003C/code> comme anomalie.\u003C/p>\n\u003Cp>Ce type de régression silencieuse est exactement ce qui se produit lors de \u003Ca href=\"/blog/deploiement-vendredi-soir-comment-eviter-la-catastrophe-seo\">déploiements mal supervisés\u003C/a> et fait partie des \u003Ca href=\"/blog/regressions-seo-les-10-types-les-plus-frequents\">régressions SEO les plus fréquentes\u003C/a> en environnement JavaScript. La détection via l'API Search Console intervient à J+3 au mieux — un outil de monitoring comme Seogard détecte ce type de divergence SSR/CSR en temps réel, avant même que Google n'indexe la page cassée.\u003C/p>\n\u003Ch2>Rapports personnalisés : aller au-delà des dashboards standards\u003C/h2>\n\u003Cp>Les dashboards Looker Studio (ex Data Studio) connectés à la Search Console souffrent des mêmes limitations que l'interface : 1 000 lignes, pas de filtres regex, pas de croisement avancé. L'API vous permet de construire des rapports qu'aucun outil standard ne propose.\u003C/p>\n\u003Ch3>Rapport de cannibalisation de mots-clés\u003C/h3>\n\u003Cp>La cannibalisation — deux pages qui se disputent la même requête — se détecte quand une requête génère des impressions sur plusieurs URLs avec des positions qui oscillent. L'interface ne permet pas cette analyse. L'API, si.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">def\u003C/span>\u003Cspan style=\"color:#B392F0\"> detect_cannibalization\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(df, min_impressions\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">50\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, min_pages\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">2\u003C/span>\u003Cspan style=\"color:#E1E4E8\">):\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    Identifie les requêtes pour lesquelles plusieurs pages se positionnent,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    signe potentiel de cannibalisation.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    Input: DataFrame avec colonnes 'query', 'page', 'clicks', 'impressions', 'position'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Agréger par query + page sur la période\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    agg \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> df.groupby([\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'query'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]).agg(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        total_clicks\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'sum'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        total_impressions\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'impressions'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'sum'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        avg_position\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'position'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'mean'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        position_std\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'position'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'std'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)  \u003C/span>\u003Cspan style=\"color:#6A737D\"># Volatilité de position\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ).reset_index()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Filtrer les requêtes avec assez d'impressions\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    agg \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> agg[agg[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'total_impressions'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">>=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> min_impressions]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Compter le nombre de pages par requête\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    pages_per_query \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> agg.groupby(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'query'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].nunique().reset_index()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    pages_per_query.columns \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'query'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page_count'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    cannibalized \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> pages_per_query[pages_per_query[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page_count'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">>=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> min_pages]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Enrichir avec les détails\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    result \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> agg.merge(cannibalized[[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'query'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]], \u003C/span>\u003Cspan style=\"color:#FFAB70\">on\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'query'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    result \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> result.sort_values([\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'query'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'total_impressions'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">], \u003C/span>\u003Cspan style=\"color:#FFAB70\">ascending\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">[\u003C/span>\u003Cspan style=\"color:#79B8FF\">True\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">False\u003C/span>\u003Cspan style=\"color:#E1E4E8\">])\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Score de cannibalisation : forte si les positions sont proches\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # et les impressions réparties entre les pages\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> query \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> result[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'query'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].unique():\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        query_data \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> result[result[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'query'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">==\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> query]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        if\u003C/span>\u003Cspan style=\"color:#79B8FF\"> len\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(query_data) \u003C/span>\u003Cspan style=\"color:#F97583\">>=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 2\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            top_two \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> query_data.head(\u003C/span>\u003Cspan style=\"color:#79B8FF\">2\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            position_gap \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> abs\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(top_two.iloc[\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">][\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'avg_position'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">-\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> top_two.iloc[\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">][\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'avg_position'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">])\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            impression_ratio \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> top_two.iloc[\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">][\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'total_impressions'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">/\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> top_two.iloc[\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">][\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'total_impressions'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">            # Cannibalisation sévère : positions proches ET impressions réparties\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">            if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> position_gap \u003C/span>\u003Cspan style=\"color:#F97583\">&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 5\u003C/span>\u003Cspan style=\"color:#F97583\"> and\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> impression_ratio \u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0.3\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">                result.loc[result[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'query'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">==\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> query, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'severity'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'high'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">            else\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">                result.loc[result[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'query'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">==\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> query, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'severity'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'low'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> result\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Exécution\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">cannib \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> detect_cannibalization(df, \u003C/span>\u003Cspan style=\"color:#FFAB70\">min_impressions\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">100\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">high_severity \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> cannib[cannib[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'severity'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">==\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'high'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">print\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#F97583\">f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#79B8FF\">{\u003C/span>\u003Cspan style=\"color:#E1E4E8\">high_severity[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'query'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].nunique()\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> requêtes avec cannibalisation sévère\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Ce rapport, exécuté mensuellement, alimente directement la stratégie de \u003Ca href=\"/blog/internal-linking-pour-l-e-commerce-strategies-avancees\">maillage interne\u003C/a> : les pages cannibalisées sont candidates à une consolidation (redirect 301 + fusion de contenu) ou à une différenciation intentionnelle.\u003C/p>\n\u003Ch3>Rapport de contenu \"zombie\"\u003C/h3>\n\u003Cp>Croisez les données GSC avec votre crawl Screaming Frog pour identifier les pages indexées qui ne génèrent ni clic ni impression — un travail impossible manuellement au-delà de quelques centaines de pages.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">def\u003C/span>\u003Cspan style=\"color:#B392F0\"> find_zombie_pages\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(gsc_df, crawl_export_csv, min_days\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">90\u003C/span>\u003Cspan style=\"color:#E1E4E8\">):\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    Croise les données GSC (16 mois) avec l'export Screaming Frog\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    pour trouver les pages indexables sans aucune visibilité.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    crawl \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> pd.read_csv(crawl_export_csv)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Pages indexables selon le crawl\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    indexable \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> crawl[\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        (crawl[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Status Code'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">==\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 200\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">&#x26;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        (crawl[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Indexability'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">==\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'Indexable'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ][\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Address'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].tolist()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Pages ayant au moins 1 impression sur la période\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    visible \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> set\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(gsc_df[gsc_df[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'impressions'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">][\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].unique())\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Zombies = indexables sans aucune impression\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    zombies \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [url \u003C/span>\u003Cspan style=\"color:#F97583\">for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> url \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> indexable \u003C/span>\u003Cspan style=\"color:#F97583\">if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> url \u003C/span>\u003Cspan style=\"color:#F97583\">not\u003C/span>\u003Cspan style=\"color:#F97583\"> in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> visible]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    print\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#F97583\">f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#79B8FF\">{len\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(zombies)\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> pages zombies sur \u003C/span>\u003Cspan style=\"color:#79B8FF\">{len\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(indexable)\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> indexables \"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">          f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"(\u003C/span>\u003Cspan style=\"color:#79B8FF\">{len\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(zombies)\u003C/span>\u003Cspan style=\"color:#F97583\">/\u003C/span>\u003Cspan style=\"color:#79B8FF\">len\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(indexable)\u003C/span>\u003Cspan style=\"color:#F97583\">*\u003C/span>\u003Cspan style=\"color:#79B8FF\">100\u003C/span>\u003Cspan style=\"color:#F97583\">:.1f\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\">%)\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> zombies\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Sur un e-commerce de 15 000 pages, ce script révèle typiquement 20 à 35 % de pages zombies — souvent des pages produit en rupture de stock, des variations de filtres mal canonicalisées, ou du \u003Ca href=\"/blog/thin-content-quand-vos-pages-nuisent-au-seo-global\">contenu thin\u003C/a> généré par des combinaisons de facettes. Ces pages grignotent le crawl budget sans aucun retour.\u003C/p>\n\u003Ch2>Automatiser l'exécution et la distribution\u003C/h2>\n\u003Cp>Un script qui tourne manuellement sur votre machine n'est pas de l'automatisation. Pour qu'un reporting soit réellement utile, il doit tourner seul, alerter quand les seuils sont franchis, et distribuer les résultats aux bonnes personnes.\u003C/p>\n\u003Ch3>Orchestration avec GitHub Actions\u003C/h3>\n\u003Cp>Pour les équipes qui n'ont pas d'infrastructure cron dédiée, GitHub Actions offre un runner gratuit (2 000 minutes/mois sur les repos privés) avec la gestion native des secrets pour les credentials.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># .github/workflows/gsc-reporting.yml\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">Weekly GSC Report\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">on\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">  schedule\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    - \u003C/span>\u003Cspan style=\"color:#85E89D\">cron\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'0 6 * * 1'\u003C/span>\u003Cspan style=\"color:#6A737D\">  # Chaque lundi à 6h UTC\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">  workflow_dispatch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:  \u003C/span>\u003Cspan style=\"color:#6A737D\"># Permet l'exécution manuelle\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">jobs\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">  generate-report\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">    runs-on\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">ubuntu-latest\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">    steps\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      - \u003C/span>\u003Cspan style=\"color:#85E89D\">uses\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">actions/checkout@v4\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      - \u003C/span>\u003Cspan style=\"color:#85E89D\">name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">Setup Python\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">        uses\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">actions/setup-python@v5\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">        with\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          python-version\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'3.12'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      - \u003C/span>\u003Cspan style=\"color:#85E89D\">name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">Install dependencies\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">        run\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">pip install google-api-python-client google-auth pandas\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      - \u003C/span>\u003Cspan style=\"color:#85E89D\">name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">Write credentials\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">        run\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">echo '${{ secrets.GSC_CREDENTIALS_JSON }}' > gsc-credentials.json\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      - \u003C/span>\u003Cspan style=\"color:#85E89D\">name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">Run report\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">        run\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">python scripts/weekly_gsc_report.py\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">        env\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          SLACK_WEBHOOK\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">${{ secrets.SLACK_WEBHOOK }}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          SITE_URL\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'sc-domain:monsite-ecommerce.fr'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      - \u003C/span>\u003Cspan style=\"color:#85E89D\">name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">Upload report artifact\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">        uses\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">actions/upload-artifact@v4\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">        with\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">gsc-report-${{ github.run_number }}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          path\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">reports/\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          retention-days\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">90\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>L'intégration dans la CI/CD n'est pas un luxe : c'est la suite logique de la démarche d'\u003Ca href=\"/blog/automatiser-les-checks-seo-dans-le-ci-cd\">automatisation des checks SEO\u003C/a> que toute équipe technique devrait avoir en place.\u003C/p>\n\u003Ch3>Alerting Slack avec seuils configurables\u003C/h3>\n\u003Cp>La partie alerting est ce qui transforme un reporting passif en système de surveillance actif. Les \u003Ca href=\"/blog/alertes-seo-quels-seuils-et-quelle-frequence\">seuils d'alerte\u003C/a> doivent être calibrés au contexte du site — un site média avec une forte saisonnalité n'aura pas les mêmes seuils qu'un SaaS B2B.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> json\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> urllib.request\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">def\u003C/span>\u003Cspan style=\"color:#B392F0\"> send_slack_alert\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(webhook_url, regressions_df, threshold_pct\u003C/span>\u003Cspan style=\"color:#F97583\">=-\u003C/span>\u003Cspan style=\"color:#79B8FF\">20\u003C/span>\u003Cspan style=\"color:#E1E4E8\">):\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    Envoie une alerte Slack si des pages franchissent le seuil de régression.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    critical \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> regressions_df[regressions_df[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks_delta_pct'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">&#x3C;\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> threshold_pct]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> critical.empty:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        return\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    total_lost_clicks \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> int\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        critical[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks_prev'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].sum() \u003C/span>\u003Cspan style=\"color:#F97583\">-\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> critical[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].sum()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    )\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    blocks \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            \"type\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"header\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            \"text\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                \"type\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"plain_text\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                \"text\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#F97583\">f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"🚨 \u003C/span>\u003Cspan style=\"color:#79B8FF\">{len\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(critical)\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> pages en régression SEO\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            \"type\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"section\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            \"text\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                \"type\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"mrkdwn\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                \"text\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#F97583\">f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"*\u003C/span>\u003Cspan style=\"color:#79B8FF\">{\u003C/span>\u003Cspan style=\"color:#E1E4E8\">total_lost_clicks\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> clics perdus* cette semaine vs semaine précédente.\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\n\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">                        f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"Seuil d'alerte : \u003C/span>\u003Cspan style=\"color:#79B8FF\">{\u003C/span>\u003Cspan style=\"color:#E1E4E8\">threshold_pct\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\">%\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Top 5 des pires régressions\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> _, row \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> critical.head(\u003C/span>\u003Cspan style=\"color:#79B8FF\">5\u003C/span>\u003Cspan style=\"color:#E1E4E8\">).iterrows():\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        blocks.append({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            \"type\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"section\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            \"text\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                \"type\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"mrkdwn\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                \"text\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#F97583\">f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"• `\u003C/span>\u003Cspan style=\"color:#79B8FF\">{\u003C/span>\u003Cspan style=\"color:#E1E4E8\">row[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\n\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">                        f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"  Clics : \u003C/span>\u003Cspan style=\"color:#79B8FF\">{int\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(row[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks_prev'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">])\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> → \u003C/span>\u003Cspan style=\"color:#79B8FF\">{int\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(row[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">])\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">                        f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"(\u003C/span>\u003Cspan style=\"color:#79B8FF\">{\u003C/span>\u003Cspan style=\"color:#E1E4E8\">row[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks_delta_pct'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\">%) | \"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">                        f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"Position : \u003C/span>\u003Cspan style=\"color:#79B8FF\">{\u003C/span>\u003Cspan style=\"color:#E1E4E8\">row[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'avg_position_prev'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003Cspan style=\"color:#F97583\">:.1f\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> → \u003C/span>\u003Cspan style=\"color:#79B8FF\">{\u003C/span>\u003Cspan style=\"color:#E1E4E8\">row[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'avg_position'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003Cspan style=\"color:#F97583\">:.1f\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        })\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    payload \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> json.dumps({\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"blocks\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: blocks}).encode(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'utf-8'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    req \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> urllib.request.Request(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        webhook_url,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        data\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">payload,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        headers\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Content-Type'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'application/json'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    )\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    urllib.request.urlopen(req)\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch2>Aller plus loin : combiner l'API GSC avec d'autres sources\u003C/h2>\n\u003Cp>L'API Search Console seule donne la visibilité organique. Combinée à d'autres sources de données, elle devient un véritable système de diagnostic.\u003C/p>\n\u003Ch3>Croisement avec les logs serveur\u003C/h3>\n\u003Cp>L'API vous dit combien Google affiche et clique vos pages. Les logs serveur vous disent combien Googlebot les crawle. Le croisement révèle trois catégories critiques :\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>Crawlé mais invisible\u003C/strong> : Googlebot visite la page régulièrement, mais elle n'apparaît jamais dans les résultats. Problème d'indexabilité (noindex non détecté, canonical vers une autre page, contenu dupliqué).\u003C/li>\n\u003Cli>\u003Cstrong>Visible mais non crawlé récemment\u003C/strong> : la page génère des impressions, mais le dernier crawl remonte à 30+ jours. Risque de désindexation progressive.\u003C/li>\n\u003Cli>\u003Cstrong>Ni crawlé ni visible\u003C/strong> : page orpheline ou enterrée dans l'architecture. Le \u003Ca href=\"/blog/architecture-de-site-seo-flat-vs-deep-structure\">choix de structure flat vs deep\u003C/a> influence directement ce ratio.\u003C/li>\n\u003C/ul>\n\u003Ch3>Croisement avec l'API URL Inspection\u003C/h3>\n\u003Cp>L'API URL Inspection (disponible depuis 2022) permet de vérifier programmatiquement le statut d'indexation de chaque URL. Limitée à 2 000 requêtes/jour/propriété, elle est trop lente pour un crawl complet mais parfaite pour vérifier les URLs identifiées comme problématiques par votre rapport GSC.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">def\u003C/span>\u003Cspan style=\"color:#B392F0\"> check_indexation_status\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(service, site_url, urls):\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    Vérifie le statut d'indexation via l'API URL Inspection.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    Attention : 2000 requêtes/jour max par propriété.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    results \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> []\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> url \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> urls[:\u003C/span>\u003Cspan style=\"color:#79B8FF\">2000\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]:  \u003C/span>\u003Cspan style=\"color:#6A737D\"># Hard limit\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        try\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            response \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> service.urlInspection().index().inspect({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                'inspectionUrl'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: url,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                'siteUrl'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: site_url\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            }).execute()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            result \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> response.get(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'inspectionResult'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, {})\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            index_status \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> result.get(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'indexStatusResult'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, {})\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            results.append({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                'url'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: url,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                'verdict'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: index_status.get(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'verdict'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'UNKNOWN'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                'coverage_state'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: index_status.get(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'coverageState'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">''\u003C/span>\u003Cspan style=\"color:#E1E4E8\">),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                'robots_txt_state'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: index_status.get(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'robotsTxtState'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">''\u003C/span>\u003Cspan style=\"color:#E1E4E8\">),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                'last_crawl_time'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: index_status.get(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'lastCrawlTime'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">''\u003C/span>\u003Cspan style=\"color:#E1E4E8\">),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                'page_fetch_state'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: index_status.get(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'pageFetchState'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">''\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            })\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            time.sleep(\u003C/span>\u003Cspan style=\"color:#79B8FF\">0.5\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)  \u003C/span>\u003Cspan style=\"color:#6A737D\"># Respecter le quota\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        except\u003C/span>\u003Cspan style=\"color:#79B8FF\"> Exception\u003C/span>\u003Cspan style=\"color:#F97583\"> as\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> e:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            results.append({\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'url'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: url, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'verdict'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'ERROR'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'error'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">str\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(e)})\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> pd.DataFrame(results)\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Ce croisement vous donne une image complète : performance de recherche (GSC Analytics API) + statut d'indexation (URL Inspection API) + comportement de crawl (logs). C'est la base d'un \u003Ca href=\"/blog/monitoring-seo-pourquoi-les-audits-ponctuels-ne-suffisent-plus\">monitoring SEO continu et actionnable\u003C/a>, bien au-delà de ce que permettent les rapports ponctuels.\u003C/p>\n\u003Cp>Les données brutes de la Search Console, enfin exploitées sans limite de lignes ni de dimensions, deviennent le socle d'un système de surveillance SEO qui détecte les problèmes avant qu'ils n'impactent le trafic. Pour les régressions qui nécessitent une détection en temps réel — un SSR cassé, un canonical qui disparaît après un déploiement — combiner ces rapports API avec un monitoring côté crawl comme Seogard couvre l'intégralité du spectre, du diagnostic post-hoc à l'alerte instantanée.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,12,[18,19,20,21],"api","search-console","reporting","automatisation","Search Console API : automatiser le reporting SEO","Wed Apr 08 2026 18:02:16 GMT+0000 (Coordinated Universal Time)",[25],{"_id":26,"slug":27,"__v":6,"author":7,"canonical":28,"category":10,"createdAt":29,"date":12,"description":30,"image":15,"imageAlt":15,"readingTime":16,"tags":31,"title":36,"updatedAt":37},"69d65fe6aa6b273b0c5b37af","mesurer-l-impact-seo-technique-quels-kpis-suivre","https://seogard.io/blog/mesurer-l-impact-seo-technique-quels-kpis-suivre","2026-04-08T14:02:14.006Z","Quels KPIs suivre pour mesurer la santé SEO technique d'un site. Crawl, indexation, Core Web Vitals, codes HTTP : métriques actionnables et méthodes de suivi.",[32,33,34,35],"kpi","métriques","seo-technique","monitoring","KPIs SEO technique : les métriques qui comptent vraiment","Wed Apr 08 2026 14:02:14 GMT+0000 (Coordinated Universal Time)"]