[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fIAM9fosukX4EbFHnSsWTubUEDZwJYjkW-iIw2bGtZ18":3,"$fCpOnIOmU5a0ErdKQx77M3PgkNVAibOQ5mtiYK02AWcw":25},{"_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":23,"updatedAt":24},"6a0c7ba7aa6b273b0c304bbe","how-to-build-custom-seo-reports-with-claude-code-and-google-search-console",0,"Equipe Seogard","Les rapports SEO pour stakeholders sont un gouffre de temps. Exporter les CSV depuis Search Console, pivoter dans Google Sheets, copier-coller dans Slides, recommencer chaque semaine. Avec Claude Code connecté directement à l'API GSC, vous pouvez construire un pipeline qui génère un rapport HTML complet — visualisations incluses — en une seule commande.\n\n## Pourquoi Looker Studio ne suffit plus\n\nLooker Studio (ex Data Studio) reste l'outil par défaut pour visualiser les données GSC. Le problème : il impose un cadre rigide. Vous voulez croiser les données de positionnement avec un export Screaming Frog pour identifier les pages qui perdent des positions ET qui ont des problèmes techniques ? Bonne chance avec un connecteur natif.\n\nLes limites concrètes qui justifient de passer à un pipeline programmatique :\n\n- **Pas de logique conditionnelle** : impossible de générer un commentaire automatique du type \"La catégorie /chaussures-running/ a perdu 23% de clics WoW, probablement lié au déréférencement de 47 URLs détecté le 12 mai\".\n- **Pas de croisement avec des sources externes** : données Screaming Frog, logs serveur, exports Ahrefs, données de monitoring Seogard — tout ça reste cloisonné.\n- **Pas de personnalisation par audience** : votre VP Marketing ne veut pas le même rapport que votre Lead Dev. Looker Studio vous force à maintenir N dashboards.\n\nClaude Code change la donne parce qu'il peut exécuter du code Python ou TypeScript, appeler des APIs, manipuler des fichiers, et produire un output structuré — le tout dans une boucle conversationnelle où vous affinez le résultat itérativement.\n\n## Connecter l'API Google Search Console\n\n### Créer les credentials OAuth 2.0\n\nL'API GSC utilise OAuth 2.0. Pas de clé API simple. Vous devez passer par la [Google Cloud Console](https://console.cloud.google.com/apis/credentials) pour créer un client OAuth de type \"Desktop application\".\n\n```bash\n# 1. Activer l'API Search Console dans votre projet GCP\ngcloud services enable searchconsole.googleapis.com\n\n# 2. Créer le client OAuth (ou le faire via l'interface Cloud Console)\n# Télécharger le fichier client_secret.json\n\n# 3. Installer les dépendances Python\npip install google-auth-oauthlib google-api-python-client pandas matplotlib\n```\n\n### Le script d'authentification et de récupération\n\nVoici le script Python complet que Claude Code peut exécuter. L'astuce : structurer la réponse de l'API en DataFrame pandas dès le départ, ce qui facilite toutes les manipulations ultérieures.\n\n```python\nimport os\nimport json\nimport pandas as pd\nfrom datetime import datetime, timedelta\nfrom google.oauth2.credentials import Credentials\nfrom google_auth_oauthlib.flow import InstalledAppFlow\nfrom googleapiclient.discovery import build\n\nSCOPES = ['https://www.googleapis.com/auth/webmasters.readonly']\nPROPERTY = 'sc-domain:votresite.fr'  # ou 'https://www.votresite.fr/'\n\ndef get_gsc_service():\n    \"\"\"Authentification OAuth avec cache du token.\"\"\"\n    creds = None\n    if os.path.exists('token.json'):\n        creds = Credentials.from_authorized_user_file('token.json', SCOPES)\n    if not creds or not creds.valid:\n        flow = InstalledAppFlow.from_client_secrets_file('client_secret.json', SCOPES)\n        creds = flow.run_local_server(port=0)\n        with open('token.json', 'w') as token:\n            token.write(creds.to_json())\n    return build('searchconsole', 'v1', credentials=creds)\n\ndef fetch_gsc_data(service, start_date, end_date, dimensions=['query', 'page'], row_limit=25000):\n    \"\"\"Récupère les données GSC avec pagination automatique.\"\"\"\n    all_rows = []\n    start_row = 0\n    \n    while True:\n        request = {\n            'startDate': start_date,\n            'endDate': end_date,\n            'dimensions': dimensions,\n            'rowLimit': row_limit,\n            'startRow': start_row,\n            'dataState': 'final'  # Exclure les données partielles\n        }\n        response = service.searchanalytics().query(\n            siteUrl=PROPERTY, body=request\n        ).execute()\n        \n        rows = response.get('rows', [])\n        if not rows:\n            break\n            \n        for row in rows:\n            data = {}\n            for i, dim in enumerate(dimensions):\n                data[dim] = row['keys'][i]\n            data['clicks'] = row['clicks']\n            data['impressions'] = row['impressions']\n            data['ctr'] = row['ctr']\n            data['position'] = row['position']\n            all_rows.append(data)\n        \n        start_row += row_limit\n        if len(rows) \u003C row_limit:\n            break\n    \n    return pd.DataFrame(all_rows)\n\n# Période : 28 derniers jours vs 28 jours précédents\ntoday = datetime.now().date()\nend_current = today - timedelta(days=3)  # Données GSC = 3 jours de latence\nstart_current = end_current - timedelta(days=27)\nend_previous = start_current - timedelta(days=1)\nstart_previous = end_previous - timedelta(days=27)\n\nservice = get_gsc_service()\ndf_current = fetch_gsc_data(service, str(start_current), str(end_current))\ndf_previous = fetch_gsc_data(service, str(start_previous), str(end_previous))\n\nprint(f\"Période actuelle : {len(df_current)} lignes\")\nprint(f\"Période précédente : {len(df_previous)} lignes\")\n```\n\nPoint important sur la pagination : l'API GSC retourne un maximum de 25 000 lignes par requête. Pour un e-commerce de 15 000 pages avec un catalogue diversifié, vous atteindrez facilement 50 000+ combinaisons query/page. La boucle `while` ci-dessus gère ça automatiquement, mais gardez en tête que chaque appel consomme du quota API (1 200 requêtes/minute selon la [documentation officielle](https://developers.google.com/webmaster-tools/limits)).\n\n### Le piège du `dataState`\n\nLe paramètre `dataState: 'final'` est crucial. Sans lui, vous récupérez les données \"fraîches\" des dernières 48-72 heures, qui sont incomplètes et fluctuantes. Les rapports basés sur des données partielles génèrent des faux positifs (\"on a perdu 40% de clics hier !\" — non, les données ne sont simplement pas consolidées). C'est le genre de détail qui différencie un rapport fiable d'un artefact anxiogène.\n\n## Construire les analyses automatisées avec Claude Code\n\nUne fois les données en DataFrame, vous entrez dans le territoire où Claude Code brille. Au lieu d'écrire vous-même les fonctions d'analyse, vous demandez à Claude Code de les générer en décrivant le résultat attendu.\n\n### Le prompt engineering pour des analyses SEO précises\n\nLa qualité du rapport dépend entièrement de la précision de vos instructions. Voici un exemple de prompt structuré à passer à Claude Code après l'exécution du script de récupération :\n\n```\nAnalyse les DataFrames df_current et df_previous.\nGénère les analyses suivantes :\n\n1. TOP MOVERS : les 20 pages avec la plus grande variation absolue de clics (positif et négatif), avec le delta de position moyenne associé.\n\n2. CANNIBALISATION : identifie les queries pour lesquelles plus de 3 pages différentes reçoivent des impressions, avec un CTR moyen \u003C 2%. Trie par impressions décroissantes.\n\n3. OPPORTUNITÉS QUICK WIN : pages positionnées entre 5 et 15 en moyenne, avec plus de 500 impressions sur la période, et un CTR inférieur à la moyenne de leur tranche de position. Ce sont les pages qui underperforment par rapport à leur visibilité.\n\n4. ANALYSE PAR RÉPERTOIRE : agrège clics, impressions, CTR moyen et position moyenne par premier niveau de répertoire URL (ex: /category/, /blog/, /product/). Compare WoW.\n\nOutput : un dictionnaire Python avec les 4 DataFrames résultants, nommés top_movers, cannibalization, quick_wins, directory_analysis.\n```\n\nClaude Code génère et exécute le code correspondant. Le résultat est un ensemble de DataFrames prêts à être transformés en visualisations ou en HTML.\n\n### Scénario concret : e-commerce de 12 000 pages\n\nPrenons le cas d'un site e-commerce spécialisé en équipement outdoor — 12 000 pages indexées, 3 200 pages de produits actifs, 180 pages de catégories, un blog de 400 articles. L'équipe SEO de 2 personnes passe chaque lundi matin à préparer un rapport hebdomadaire pour le comité de direction.\n\nAvec le pipeline Claude Code + GSC, voici ce qui change :\n\n**Avant** : 3 heures de travail manuel. Export GSC → Google Sheets → nettoyage → tableaux croisés dynamiques → copier dans Slides → rédaction des commentaires. Le rapport est livré mardi.\n\n**Après** : 15 minutes. Exécution du script → Claude Code analyse les données et génère les commentaires → rapport HTML exporté en PDF. Le rapport est prêt lundi à 9h15.\n\nSur cette volumétrie, l'API GSC retourne environ 85 000 lignes de données (query × page) sur 28 jours. Le script de récupération fait 4 appels API, terminés en moins de 10 secondes. L'analyse par Claude Code prend environ 30 secondes.\n\nL'analyse par répertoire révèle par exemple que le répertoire `/randonnee/` a perdu 18% de clics WoW, corrélé avec une dégradation de position moyenne de 4.2 → 5.8. L'analyse `top_movers` identifie que 6 des 10 pages les plus impactées sont des fiches produit qui ont perdu leur FAQ structured data suite au [retrait des résultats enrichis FAQ par Google](/blog/google-to-no-longer-support-faq-rich-results). Ce type de corrélation automatisée est impossible dans Looker Studio.\n\n## Générer des visualisations custom\n\nLes graphiques Matplotlib/Seaborn conviennent pour des rapports internes. Pour des rapports stakeholders, vous avez besoin de quelque chose de plus propre. Deux approches.\n\n### Approche 1 : Matplotlib avec style custom\n\n```python\nimport matplotlib.pyplot as plt\nimport matplotlib.ticker as mticker\nimport numpy as np\n\ndef plot_directory_comparison(directory_analysis):\n    \"\"\"Graphique de comparaison WoW par répertoire.\"\"\"\n    fig, axes = plt.subplots(1, 2, figsize=(16, 8))\n    fig.suptitle('Performance par répertoire — Semaine du 12 au 18 mai 2026', \n                 fontsize=14, fontweight='bold', y=1.02)\n    \n    df = directory_analysis.sort_values('clicks_current', ascending=True).tail(15)\n    \n    # Graphique barres horizontales : clics current vs previous\n    y_pos = np.arange(len(df))\n    axes[0].barh(y_pos - 0.2, df['clicks_previous'], 0.4, \n                 label='S-1', color='#94a3b8', alpha=0.8)\n    axes[0].barh(y_pos + 0.2, df['clicks_current'], 0.4, \n                 label='S en cours', color='#3b82f6', alpha=0.8)\n    axes[0].set_yticks(y_pos)\n    axes[0].set_yticklabels(df['directory'], fontsize=10)\n    axes[0].set_xlabel('Clics')\n    axes[0].set_title('Clics par répertoire')\n    axes[0].legend()\n    axes[0].xaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f'{int(x):,}'))\n    \n    # Graphique delta : variation en %\n    colors = ['#ef4444' if x \u003C 0 else '#22c55e' for x in df['clicks_delta_pct']]\n    axes[1].barh(y_pos, df['clicks_delta_pct'], color=colors, alpha=0.8)\n    axes[1].set_yticks(y_pos)\n    axes[1].set_yticklabels(df['directory'], fontsize=10)\n    axes[1].set_xlabel('Variation (%)')\n    axes[1].set_title('Variation WoW')\n    axes[1].axvline(x=0, color='#64748b', linewidth=0.8)\n    \n    for i, (val, dir_name) in enumerate(zip(df['clicks_delta_pct'], df['directory'])):\n        axes[1].text(val + (1 if val >= 0 else -1), i, f'{val:+.1f}%', \n                     va='center', fontsize=9, fontweight='bold')\n    \n    plt.tight_layout()\n    plt.savefig('directory_comparison.png', dpi=150, bbox_inches='tight')\n    plt.close()\n    \nplot_directory_comparison(directory_analysis)\n```\n\n### Approche 2 : rapport HTML complet avec Plotly\n\nPour un rapport interactif partageable par lien, Plotly génère des graphiques HTML autonomes. Claude Code peut assembler un rapport HTML complet avec les graphiques embedded :\n\n```python\nimport plotly.graph_objects as go\nfrom plotly.subplots import make_subplots\n\ndef generate_html_report(top_movers, cannibalization, quick_wins, directory_analysis):\n    \"\"\"Génère un rapport HTML standalone avec graphiques Plotly intégrés.\"\"\"\n    \n    # Graphique top movers\n    tm = top_movers.head(20)\n    fig_movers = go.Figure()\n    fig_movers.add_trace(go.Bar(\n        x=tm['page'].apply(lambda x: x.split('/')[-2] if x.endswith('/') else x.split('/')[-1]),\n        y=tm['clicks_delta'],\n        marker_color=['#ef4444' if v \u003C 0 else '#22c55e' for v in tm['clicks_delta']],\n        text=[f\"{v:+d}\" for v in tm['clicks_delta']],\n        textposition='outside'\n    ))\n    fig_movers.update_layout(\n        title='Top 20 pages — Variation de clics WoW',\n        xaxis_tickangle=-45,\n        height=500,\n        template='plotly_white'\n    )\n    \n    # Assembler le HTML\n    report_html = f\"\"\"\n    \u003C!DOCTYPE html>\n    \u003Chtml lang=\"fr\">\n    \u003Chead>\n        \u003Cmeta charset=\"UTF-8\">\n        \u003Ctitle>Rapport SEO Hebdomadaire — Semaine 20\u003C/title>\n        \u003Cscript src=\"https://cdn.plot.ly/plotly-2.35.0.min.js\">\u003C/script>\n        \u003Cstyle>\n            body {{ font-family: 'Inter', -apple-system, sans-serif; max-width: 1200px; margin: 0 auto; padding: 2rem; color: #1e293b; }}\n            h1 {{ border-bottom: 3px solid #3b82f6; padding-bottom: 0.5rem; }}\n            .metric-grid {{ display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin: 2rem 0; }}\n            .metric-card {{ background: #f8fafc; border-radius: 8px; padding: 1.5rem; text-align: center; }}\n            .metric-value {{ font-size: 2rem; font-weight: 700; color: #1e293b; }}\n            .metric-delta {{ font-size: 0.9rem; margin-top: 0.25rem; }}\n            .metric-delta.positive {{ color: #22c55e; }}\n            .metric-delta.negative {{ color: #ef4444; }}\n            table {{ width: 100%; border-collapse: collapse; margin: 1rem 0; }}\n            th, td {{ padding: 0.75rem; text-align: left; border-bottom: 1px solid #e2e8f0; }}\n            th {{ background: #f1f5f9; font-weight: 600; }}\n            .commentary {{ background: #eff6ff; border-left: 4px solid #3b82f6; padding: 1rem 1.5rem; margin: 1.5rem 0; border-radius: 0 4px 4px 0; }}\n        \u003C/style>\n    \u003C/head>\n    \u003Cbody>\n        \u003Ch1>Rapport SEO — Semaine du 12 au 18 mai 2026\u003C/h1>\n        \u003Cp>Généré automatiquement le {datetime.now().strftime('%d/%m/%Y à %H:%M')}\u003C/p>\n        \n        \u003Cdiv class=\"metric-grid\">\n            \u003Cdiv class=\"metric-card\">\n                \u003Cdiv class=\"metric-label\">Clics totaux\u003C/div>\n                \u003Cdiv class=\"metric-value\">42,847\u003C/div>\n                \u003Cdiv class=\"metric-delta negative\">-3.2% vs S-1\u003C/div>\n            \u003C/div>\n            \u003C!-- ... autres métriques ... -->\n        \u003C/div>\n        \n        \u003Ch2>Pages avec les plus fortes variations\u003C/h2>\n        \u003Cdiv id=\"chart-movers\">\u003C/div>\n        \u003Cscript>\n            var data = {fig_movers.to_json()};\n            Plotly.newPlot('chart-movers', data.data, data.layout);\n        \u003C/script>\n        \n        \u003Cdiv class=\"commentary\">\n            \u003Cstrong>Analyse automatique :\u003C/strong> 6 des 10 pages en baisse appartiennent au répertoire \n            /randonnee/. La position moyenne de ce répertoire est passée de 4.2 à 5.8. \n            Corrélation probable avec la perte des rich results FAQ sur ces pages.\n        \u003C/div>\n        \n        \u003Ch2>Opportunités Quick Win\u003C/h2>\n        \u003Ctable>\n            \u003Ctr>\u003Cth>Page\u003C/th>\u003Cth>Position moy.\u003C/th>\u003Cth>Impressions\u003C/th>\u003Cth>CTR\u003C/th>\u003Cth>CTR attendu\u003C/th>\u003C/tr>\n            \u003C!-- Injecté dynamiquement -->\n        \u003C/table>\n    \u003C/body>\n    \u003C/html>\n    \"\"\"\n    \n    with open('rapport_seo_s20.html', 'w', encoding='utf-8') as f:\n        f.write(report_html)\n    \n    return 'rapport_seo_s20.html'\n```\n\nL'avantage du HTML standalone : partageable par email, stockable dans un Google Drive, consultable sans outil spécifique. Pour une conversion en PDF, un simple `playwright pdf rapport_seo_s20.html rapport_seo_s20.pdf` fait le travail.\n\n## Automatiser les commentaires analytiques\n\nLe reporting sans analyse est un dashboard. Ce qui fait la valeur d'un rapport stakeholders, ce sont les commentaires contextuels. C'est là que Claude Code apporte un avantage décisif par rapport à un script Python classique.\n\n### Faire rédiger l'analyse par Claude Code\n\nAprès avoir calculé les métriques, vous pouvez demander à Claude Code de générer les commentaires en langage naturel. La clé : fournir le contexte business dans votre prompt.\n\nUn exemple de workflow dans Claude Code :\n\n1. Le script Python s'exécute et génère les DataFrames d'analyse\n2. Vous demandez à Claude Code : *\"Analyse les résultats. Contexte : nous avons migré 47 URLs de /equipement/chaussures/ vers /chaussures-trail/ le 8 mai. Rédige un paragraphe de synthèse pour le comité de direction, en français, max 150 mots, focus sur l'impact business.\"*\n3. Claude Code produit : *\"La migration des 47 URLs chaussures, effective le 8 mai, montre des signaux encourageants à J+10. Les nouvelles URLs captent 78% du trafic des anciennes (1 240 clics vs 1 590 pré-migration). 3 URLs n'ont pas encore été réindexées — vérification des redirections 301 recommandée. La catégorie /chaussures-trail/ gagne 2 positions en moyenne sur les requêtes \"chaussures trail [marque]\". Projection à J+30 : retour au niveau pré-migration si les 3 URLs bloquées sont corrigées cette semaine.\"*\n\nCe type de commentaire contextuel est impossible à automatiser avec Looker Studio ou Google Sheets. C'est du traitement du langage sur des données structurées, exactement ce pour quoi les LLM sont pertinents.\n\n### Edge case : quand l'analyse automatique se trompe\n\nUn LLM peut sur-interpréter une corrélation. Si le trafic d'une catégorie baisse la semaine d'un long weekend, Claude Code pourrait attribuer ça à un problème SEO alors que c'est simplement saisonnier. \n\nDeux garde-fous :\n\n- **Injectez les données de l'année précédente** pour la même période. Si le pattern est similaire, c'est saisonnier.\n- **Ajoutez dans votre prompt** : \"Avant d'attribuer une cause, vérifie si la variation est cohérente avec la saisonnalité (données Y-1 fournies) et avec les variations du marché (données par device et par pays).\"\n\nNe faites jamais confiance aveuglément à un commentaire auto-généré. Relisez-le, surtout avant de l'envoyer à votre C-level.\n\n## Intégrer des sources de données tierces\n\nLe vrai potentiel de ce pipeline apparaît quand vous croisez les données GSC avec d'autres sources. Voici les croisements les plus utiles.\n\n### GSC + Screaming Frog : identifier les régressions techniques qui impactent le trafic\n\nExportez un crawl Screaming Frog en CSV (`Internal_All.csv`) et croisez avec les données GSC :\n\n```python\nimport pandas as pd\n\n# Charger l'export Screaming Frog\nsf = pd.read_csv('Internal_All.csv', usecols=[\n    'Address', 'Status Code', 'Indexability', 'Title 1', \n    'Meta Description 1', 'H1-1', 'Canonical Link Element 1',\n    'Word Count'\n])\n\n# Normaliser les URLs pour le join\nsf['page'] = sf['Address'].str.rstrip('/')\ndf_current['page_clean'] = df_current['page'].str.rstrip('/')\n\n# Croisement\nmerged = df_current.merge(sf, left_on='page_clean', right_on='page', how='left')\n\n# Pages avec du trafic GSC mais un problème technique\nissues = merged[\n    (merged['clicks'] > 10) & \n    (\n        (merged['Indexability'] == 'Non-Indexable') |\n        (merged['Status Code'] != 200) |\n        (merged['Canonical Link Element 1'] != merged['Address'])\n    )\n]\n\nprint(f\"ALERTE : {len(issues)} pages avec du trafic ont un problème technique\")\nprint(issues[['page_clean', 'clicks', 'Status Code', 'Indexability', 'Canonical Link Element 1']].to_string())\n```\n\nSur notre e-commerce outdoor de 12 000 pages, ce croisement a identifié 23 pages produit qui généraient encore 340 clics/semaine mais retournaient un 301 vers une page de catégorie générique — résultat d'une mise à jour de catalogue bâclée. 340 clics × taux de conversion moyen de 2.1% × panier moyen de 89€ = 640€ de revenu hebdomadaire en danger.\n\nCe genre de croisement est exactement le type de régression qu'un outil comme Seogard détecte automatiquement et en continu, sans attendre le rapport hebdomadaire.\n\n### GSC + données AI Search\n\nAvec l'expansion des liens dans les résultats AI de Google, il devient pertinent de croiser vos données GSC avec votre visibilité dans les AI Overviews. Si vous trackez les requêtes où vos pages apparaissent dans les [résultats de recherche AI](/blog/google-s-ai-search-now-shows-more-links-what-seos-need-to-know-via-sejournal-mattgsouthern), vous pouvez comparer le CTR de ces requêtes avec celles qui déclenchent des résultats classiques.\n\nLe challenge actuel : Google ne fournit [toujours pas de données de clics spécifiques aux AI Overviews](/blog/google-adds-more-ai-search-links-still-no-click-data-for-seos-via-sejournal-mattgsouthern) dans la GSC. Vous devez donc utiliser des proxys — des outils tiers de SERP tracking pour identifier les requêtes qui déclenchent des AI Overviews, puis croiser avec vos données GSC.\n\n## Orchestrer le workflow : du one-shot au cron job\n\nClaude Code est interactif par nature. Pour un workflow récurrent, vous avez deux options.\n\n### Option 1 : script Python autonome déclenché par cron\n\nVous utilisez Claude Code pour **développer** le script, puis vous l'exécutez de manière autonome via cron ou un scheduler comme GitHub Actions :\n\n```yaml\n# .github/workflows/seo-report.yml\nname: Weekly SEO Report\non:\n  schedule:\n    - cron: '0 7 * * 1'  # Chaque lundi à 7h UTC\n  workflow_dispatch:  # Déclenchement manuel possible\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 -r requirements.txt\n      \n      - name: Generate report\n        env:\n          GSC_CREDENTIALS: ${{ secrets.GSC_CREDENTIALS }}\n        run: python generate_report.py\n      \n      - name: Send via email\n        uses: dawidd6/action-send-mail@v3\n        with:\n          server_address: smtp.gmail.com\n          server_port: 465\n          username: ${{ secrets.MAIL_USER }}\n          password: ${{ secrets.MAIL_PASS }}\n          subject: \"Rapport SEO Hebdomadaire — Semaine ${{ env.WEEK_NUMBER }}\"\n          to: seo-team@votresite.fr,direction@votresite.fr\n          from: reports@votresite.fr\n          attachments: rapport_seo_*.html\n```\n\n### Option 2 : Claude Code en mode assisté pour l'analyse ad-hoc\n\nGardez le script de récupération des données en automatique, mais utilisez Claude Code manuellement pour l'analyse quand vous avez un contexte spécifique à injecter (migration en cours, core update Google, événement saisonnier).\n\nL'option 1 couvre 80% des besoins. L'option 2 est votre super-pouvoir pour les semaines atypiques — typiquement après un [core update qui redistribue les cartes](/blog/google-core-update-reshuffles-winners-ai-search-expands-links-seo-pulse-via-sejournal-mattgsouthern).\n\n## Les limites à connaître avant de se lancer\n\nCe pipeline n'est pas sans friction. Quelques réalités à garder en tête :\n\n**Quota API GSC** : la limite de 1 200 requêtes/minute est généreuse pour un seul site, mais si vous gérez un portefeuille de 20 domaines pour un grand compte, vous allez la toucher. Implémentez un rate limiter (`time.sleep(0.1)` entre les appels suffit dans la plupart des cas).\n\n**Latence des données GSC** : 3 jours minimum. Votre rapport du lundi reflète les données jusqu'à vendredi au mieux. Pour du monitoring en temps réel, la GSC ne suffit pas — vous avez besoin de données de crawl et de monitoring continu.\n\n**Coût Claude Code** : chaque exécution de code consomme des tokens. Pour un rapport hebdomadaire standard, comptez environ 5 000-10 000 tokens par génération. Raisonnable, mais ça s'additionne si vous itérez beaucoup.\n\n**Reproductibilité** : Claude Code peut générer un code légèrement différent à chaque exécution. Si la cohérence semaine-après-semaine est critique (et elle l'est pour un reporting C-level), figez le code généré dans un script Python versionné et ne refaites appel à Claude Code que pour les évolutions.\n\n**Sécurité** : vos credentials OAuth ne doivent jamais être passées en clair dans un prompt Claude Code. Utilisez des variables d'environnement ou un secret manager. Le fichier `token.json` généré par l'authentification OAuth donne un accès en lecture à toutes vos propriétés GSC — traitez-le comme un mot de passe.\n\n## Le pipeline en pratique : ce qui change dans votre semaine\n\nCe workflow remplace un processus de reporting fragile et chronophage par un pipeline reproductible. Les données GSC sont récupérées automatiquement, croisées avec vos exports techniques, analysées avec une logique métier que vous contrôlez, et présentées dans un format adapté à chaque audience.\n\nL'investissement initial est de 2-3 heures pour mettre en place le script, configurer l'authentification OAuth, et calibrer les seuils d'analyse. Le retour : chaque semaine, vous récupérez 2-3 heures de travail manuel, et surtout vous obtenez un rapport plus riche, plus rapide, et plus fiable que ce qu'un humain produirait en manipulant des CSV.\n\nPour les régressions critiques entre deux rapports — une meta title qui disparaît, un canonical qui casse, un bloc de pages qui passe en noindex — un outil de monitoring continu comme Seogard reste indispensable. Le rapport hebdomadaire analyse les tendances. Le monitoring temps réel attrape les urgences.\n```","https://seogard.io/blog/how-to-build-custom-seo-reports-with-claude-code-and-google-search-console","Actualités SEO","2026-05-19T15:03:03.390Z","2026-05-19","Connectez l'API GSC à Claude Code pour générer des rapports SEO automatisés avec visualisations custom et workflows flexibles.","\u003Cp>Les rapports SEO pour stakeholders sont un gouffre de temps. Exporter les CSV depuis Search Console, pivoter dans Google Sheets, copier-coller dans Slides, recommencer chaque semaine. Avec Claude Code connecté directement à l'API GSC, vous pouvez construire un pipeline qui génère un rapport HTML complet — visualisations incluses — en une seule commande.\u003C/p>\n\u003Ch2>Pourquoi Looker Studio ne suffit plus\u003C/h2>\n\u003Cp>Looker Studio (ex Data Studio) reste l'outil par défaut pour visualiser les données GSC. Le problème : il impose un cadre rigide. Vous voulez croiser les données de positionnement avec un export Screaming Frog pour identifier les pages qui perdent des positions ET qui ont des problèmes techniques ? Bonne chance avec un connecteur natif.\u003C/p>\n\u003Cp>Les limites concrètes qui justifient de passer à un pipeline programmatique :\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>Pas de logique conditionnelle\u003C/strong> : impossible de générer un commentaire automatique du type \"La catégorie /chaussures-running/ a perdu 23% de clics WoW, probablement lié au déréférencement de 47 URLs détecté le 12 mai\".\u003C/li>\n\u003Cli>\u003Cstrong>Pas de croisement avec des sources externes\u003C/strong> : données Screaming Frog, logs serveur, exports Ahrefs, données de monitoring Seogard — tout ça reste cloisonné.\u003C/li>\n\u003Cli>\u003Cstrong>Pas de personnalisation par audience\u003C/strong> : votre VP Marketing ne veut pas le même rapport que votre Lead Dev. Looker Studio vous force à maintenir N dashboards.\u003C/li>\n\u003C/ul>\n\u003Cp>Claude Code change la donne parce qu'il peut exécuter du code Python ou TypeScript, appeler des APIs, manipuler des fichiers, et produire un output structuré — le tout dans une boucle conversationnelle où vous affinez le résultat itérativement.\u003C/p>\n\u003Ch2>Connecter l'API Google Search Console\u003C/h2>\n\u003Ch3>Créer les credentials OAuth 2.0\u003C/h3>\n\u003Cp>L'API GSC utilise OAuth 2.0. Pas de clé API simple. Vous devez passer par la \u003Ca href=\"https://console.cloud.google.com/apis/credentials\">Google Cloud Console\u003C/a> pour créer un client OAuth de type \"Desktop application\".\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># 1. Activer l'API Search Console dans votre projet GCP\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>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># 2. Créer le client OAuth (ou le faire via l'interface Cloud Console)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Télécharger le fichier client_secret.json\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># 3. Installer les dépendances Python\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">pip\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> install\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> google-auth-oauthlib\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> google-api-python-client\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> pandas\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> matplotlib\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch3>Le script d'authentification et de récupération\u003C/h3>\n\u003Cp>Voici le script Python complet que Claude Code peut exécuter. L'astuce : structurer la réponse de l'API en DataFrame pandas dès le départ, ce qui facilite toutes les manipulations ultérieures.\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\"> os\u003C/span>\u003C/span>\n\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\"> pandas \u003C/span>\u003Cspan style=\"color:#F97583\">as\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> pd\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> datetime \u003C/span>\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> datetime, timedelta\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> google.oauth2.credentials \u003C/span>\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> Credentials\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> google_auth_oauthlib.flow \u003C/span>\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> InstalledAppFlow\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\">PROPERTY\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'sc-domain:votresite.fr'\u003C/span>\u003Cspan style=\"color:#6A737D\">  # ou 'https://www.votresite.fr/'\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:#9ECBFF\">    \"\"\"Authentification OAuth avec cache du token.\"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    creds \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> None\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> os.path.exists(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'token.json'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">):\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        creds \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> Credentials.from_authorized_user_file(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'token.json'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">SCOPES\u003C/span>\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\"> creds \u003C/span>\u003Cspan style=\"color:#F97583\">or\u003C/span>\u003Cspan style=\"color:#F97583\"> not\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> creds.valid:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        flow \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> InstalledAppFlow.from_client_secrets_file(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'client_secret.json'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">SCOPES\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        creds \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> flow.run_local_server(\u003C/span>\u003Cspan style=\"color:#FFAB70\">port\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        with\u003C/span>\u003Cspan style=\"color:#79B8FF\"> open\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'token.json'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'w'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">as\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> token:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            token.write(creds.to_json())\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\">creds)\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, start_date, end_date, 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\">], 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\">    \"\"\"Récupère les données GSC avec pagination automatique.\"\"\"\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\">    start_row \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    while\u003C/span>\u003Cspan style=\"color:#79B8FF\"> True\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        request \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\">: start_date,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            'endDate'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: end_date,\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\">            'startRow'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: start_row,\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\">  # Exclure les données partielles\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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.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:#79B8FF\">PROPERTY\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">body\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">request\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\">        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\">        if\u003C/span>\u003Cspan style=\"color:#F97583\"> not\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> rows:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">            break\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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\">            data \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\">                data[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\">            data[\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\">            data[\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\">            data[\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\">            data[\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\">            all_rows.append(data)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        start_row \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:#F97583\">        if\u003C/span>\u003Cspan style=\"color:#79B8FF\"> len\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(rows) \u003C/span>\u003Cspan style=\"color:#F97583\">&#x3C;\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> row_limit:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">            break\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(all_rows)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Période : 28 derniers jours vs 28 jours précédents\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">today \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> datetime.now().date()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">end_current \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> today \u003C/span>\u003Cspan style=\"color:#F97583\">-\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> timedelta(\u003C/span>\u003Cspan style=\"color:#FFAB70\">days\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">3\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)  \u003C/span>\u003Cspan style=\"color:#6A737D\"># Données GSC = 3 jours de latence\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">start_current \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> end_current \u003C/span>\u003Cspan style=\"color:#F97583\">-\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> timedelta(\u003C/span>\u003Cspan style=\"color:#FFAB70\">days\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">27\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">end_previous \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> start_current \u003C/span>\u003Cspan style=\"color:#F97583\">-\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> 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\">start_previous \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> end_previous \u003C/span>\u003Cspan style=\"color:#F97583\">-\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> timedelta(\u003C/span>\u003Cspan style=\"color:#FFAB70\">days\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">27\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\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\">\u003Cspan style=\"color:#E1E4E8\">df_current \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> fetch_gsc_data(service, \u003C/span>\u003Cspan style=\"color:#79B8FF\">str\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(start_current), \u003C/span>\u003Cspan style=\"color:#79B8FF\">str\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(end_current))\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(service, \u003C/span>\u003Cspan style=\"color:#79B8FF\">str\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(start_previous), \u003C/span>\u003Cspan style=\"color:#79B8FF\">str\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(end_previous))\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\">\"Période actuelle : \u003C/span>\u003Cspan style=\"color:#79B8FF\">{len\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(df_current)\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\">\u003Cspan style=\"color:#79B8FF\">print\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#F97583\">f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"Période précédente : \u003C/span>\u003Cspan style=\"color:#79B8FF\">{len\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(df_previous)\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> lignes\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Point important sur la pagination : l'API GSC retourne un maximum de 25 000 lignes par requête. Pour un e-commerce de 15 000 pages avec un catalogue diversifié, vous atteindrez facilement 50 000+ combinaisons query/page. La boucle \u003Ccode>while\u003C/code> ci-dessus gère ça automatiquement, mais gardez en tête que chaque appel consomme du quota API (1 200 requêtes/minute selon la \u003Ca href=\"https://developers.google.com/webmaster-tools/limits\">documentation officielle\u003C/a>).\u003C/p>\n\u003Ch3>Le piège du \u003Ccode>dataState\u003C/code>\u003C/h3>\n\u003Cp>Le paramètre \u003Ccode>dataState: 'final'\u003C/code> est crucial. Sans lui, vous récupérez les données \"fraîches\" des dernières 48-72 heures, qui sont incomplètes et fluctuantes. Les rapports basés sur des données partielles génèrent des faux positifs (\"on a perdu 40% de clics hier !\" — non, les données ne sont simplement pas consolidées). C'est le genre de détail qui différencie un rapport fiable d'un artefact anxiogène.\u003C/p>\n\u003Ch2>Construire les analyses automatisées avec Claude Code\u003C/h2>\n\u003Cp>Une fois les données en DataFrame, vous entrez dans le territoire où Claude Code brille. Au lieu d'écrire vous-même les fonctions d'analyse, vous demandez à Claude Code de les générer en décrivant le résultat attendu.\u003C/p>\n\u003Ch3>Le prompt engineering pour des analyses SEO précises\u003C/h3>\n\u003Cp>La qualité du rapport dépend entièrement de la précision de vos instructions. Voici un exemple de prompt structuré à passer à Claude Code après l'exécution du script de récupération :\u003C/p>\n\u003Cpre>\u003Ccode>Analyse les DataFrames df_current et df_previous.\nGénère les analyses suivantes :\n\n1. TOP MOVERS : les 20 pages avec la plus grande variation absolue de clics (positif et négatif), avec le delta de position moyenne associé.\n\n2. CANNIBALISATION : identifie les queries pour lesquelles plus de 3 pages différentes reçoivent des impressions, avec un CTR moyen &#x3C; 2%. Trie par impressions décroissantes.\n\n3. OPPORTUNITÉS QUICK WIN : pages positionnées entre 5 et 15 en moyenne, avec plus de 500 impressions sur la période, et un CTR inférieur à la moyenne de leur tranche de position. Ce sont les pages qui underperforment par rapport à leur visibilité.\n\n4. ANALYSE PAR RÉPERTOIRE : agrège clics, impressions, CTR moyen et position moyenne par premier niveau de répertoire URL (ex: /category/, /blog/, /product/). Compare WoW.\n\nOutput : un dictionnaire Python avec les 4 DataFrames résultants, nommés top_movers, cannibalization, quick_wins, directory_analysis.\n\u003C/code>\u003C/pre>\n\u003Cp>Claude Code génère et exécute le code correspondant. Le résultat est un ensemble de DataFrames prêts à être transformés en visualisations ou en HTML.\u003C/p>\n\u003Ch3>Scénario concret : e-commerce de 12 000 pages\u003C/h3>\n\u003Cp>Prenons le cas d'un site e-commerce spécialisé en équipement outdoor — 12 000 pages indexées, 3 200 pages de produits actifs, 180 pages de catégories, un blog de 400 articles. L'équipe SEO de 2 personnes passe chaque lundi matin à préparer un rapport hebdomadaire pour le comité de direction.\u003C/p>\n\u003Cp>Avec le pipeline Claude Code + GSC, voici ce qui change :\u003C/p>\n\u003Cp>\u003Cstrong>Avant\u003C/strong> : 3 heures de travail manuel. Export GSC → Google Sheets → nettoyage → tableaux croisés dynamiques → copier dans Slides → rédaction des commentaires. Le rapport est livré mardi.\u003C/p>\n\u003Cp>\u003Cstrong>Après\u003C/strong> : 15 minutes. Exécution du script → Claude Code analyse les données et génère les commentaires → rapport HTML exporté en PDF. Le rapport est prêt lundi à 9h15.\u003C/p>\n\u003Cp>Sur cette volumétrie, l'API GSC retourne environ 85 000 lignes de données (query × page) sur 28 jours. Le script de récupération fait 4 appels API, terminés en moins de 10 secondes. L'analyse par Claude Code prend environ 30 secondes.\u003C/p>\n\u003Cp>L'analyse par répertoire révèle par exemple que le répertoire \u003Ccode>/randonnee/\u003C/code> a perdu 18% de clics WoW, corrélé avec une dégradation de position moyenne de 4.2 → 5.8. L'analyse \u003Ccode>top_movers\u003C/code> identifie que 6 des 10 pages les plus impactées sont des fiches produit qui ont perdu leur FAQ structured data suite au \u003Ca href=\"/blog/google-to-no-longer-support-faq-rich-results\">retrait des résultats enrichis FAQ par Google\u003C/a>. Ce type de corrélation automatisée est impossible dans Looker Studio.\u003C/p>\n\u003Ch2>Générer des visualisations custom\u003C/h2>\n\u003Cp>Les graphiques Matplotlib/Seaborn conviennent pour des rapports internes. Pour des rapports stakeholders, vous avez besoin de quelque chose de plus propre. Deux approches.\u003C/p>\n\u003Ch3>Approche 1 : Matplotlib avec style custom\u003C/h3>\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\"> matplotlib.pyplot \u003C/span>\u003Cspan style=\"color:#F97583\">as\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> plt\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> matplotlib.ticker \u003C/span>\u003Cspan style=\"color:#F97583\">as\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> mticker\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> numpy \u003C/span>\u003Cspan style=\"color:#F97583\">as\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> np\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\"> plot_directory_comparison\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(directory_analysis):\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"\"\"Graphique de comparaison WoW par répertoire.\"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    fig, axes \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> plt.subplots(\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">2\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">figsize\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">16\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">8\u003C/span>\u003Cspan style=\"color:#E1E4E8\">))\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    fig.suptitle(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Performance par répertoire — Semaine du 12 au 18 mai 2026'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">                 fontsize\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">14\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">fontweight\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'bold'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">y\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">1.02\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\">    df \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> directory_analysis.sort_values(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks_current'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">ascending\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">True\u003C/span>\u003Cspan style=\"color:#E1E4E8\">).tail(\u003C/span>\u003Cspan style=\"color:#79B8FF\">15\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\">    # Graphique barres horizontales : clics current vs previous\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    y_pos \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> np.arange(\u003C/span>\u003Cspan style=\"color:#79B8FF\">len\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(df))\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    axes[\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].barh(y_pos \u003C/span>\u003Cspan style=\"color:#F97583\">-\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0.2\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, df[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks_previous'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">], \u003C/span>\u003Cspan style=\"color:#79B8FF\">0.4\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">                 label\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'S-1'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">color\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'#94a3b8'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">alpha\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">0.8\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    axes[\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].barh(y_pos \u003C/span>\u003Cspan style=\"color:#F97583\">+\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0.2\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, df[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks_current'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">], \u003C/span>\u003Cspan style=\"color:#79B8FF\">0.4\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">                 label\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'S en cours'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">color\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'#3b82f6'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">alpha\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">0.8\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    axes[\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].set_yticks(y_pos)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    axes[\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].set_yticklabels(df[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'directory'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">], \u003C/span>\u003Cspan style=\"color:#FFAB70\">fontsize\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">10\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    axes[\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].set_xlabel(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Clics'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    axes[\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].set_title(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Clics par répertoire'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    axes[\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].legend()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    axes[\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].xaxis.set_major_formatter(mticker.FuncFormatter(\u003C/span>\u003Cspan style=\"color:#F97583\">lambda\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> x, _: \u003C/span>\u003Cspan style=\"color:#F97583\">f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'\u003C/span>\u003Cspan style=\"color:#79B8FF\">{int\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(x)\u003C/span>\u003Cspan style=\"color:#F97583\">:,\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:#6A737D\">    # Graphique delta : variation en %\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    colors \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'#ef4444'\u003C/span>\u003Cspan style=\"color:#F97583\"> if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> x \u003C/span>\u003Cspan style=\"color:#F97583\">&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003Cspan style=\"color:#F97583\"> else\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '#22c55e'\u003C/span>\u003Cspan style=\"color:#F97583\"> for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> x \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> df[\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\">    axes[\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].barh(y_pos, df[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks_delta_pct'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">], \u003C/span>\u003Cspan style=\"color:#FFAB70\">color\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">colors, \u003C/span>\u003Cspan style=\"color:#FFAB70\">alpha\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">0.8\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    axes[\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].set_yticks(y_pos)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    axes[\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].set_yticklabels(df[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'directory'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">], \u003C/span>\u003Cspan style=\"color:#FFAB70\">fontsize\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">10\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    axes[\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].set_xlabel(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Variation (%)'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    axes[\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].set_title(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Variation WoW'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    axes[\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].axvline(\u003C/span>\u003Cspan style=\"color:#FFAB70\">x\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">color\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'#64748b'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">linewidth\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">0.8\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\">    for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> i, (val, dir_name) \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#79B8FF\"> enumerate\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">zip\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(df[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks_delta_pct'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">], df[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'directory'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">])):\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        axes[\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].text(val \u003C/span>\u003Cspan style=\"color:#F97583\">+\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#F97583\"> if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> val \u003C/span>\u003Cspan style=\"color:#F97583\">>=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003Cspan style=\"color:#F97583\"> else\u003C/span>\u003Cspan style=\"color:#F97583\"> -\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">), i, \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\">val\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:#FFAB70\">                     va\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'center'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">fontsize\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">9\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">fontweight\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'bold'\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\">    plt.tight_layout()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    plt.savefig(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'directory_comparison.png'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">dpi\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">150\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">bbox_inches\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'tight'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    plt.close()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">plot_directory_comparison(directory_analysis)\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch3>Approche 2 : rapport HTML complet avec Plotly\u003C/h3>\n\u003Cp>Pour un rapport interactif partageable par lien, Plotly génère des graphiques HTML autonomes. Claude Code peut assembler un rapport HTML complet avec les graphiques embedded :\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\"> plotly.graph_objects \u003C/span>\u003Cspan style=\"color:#F97583\">as\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> go\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> plotly.subplots \u003C/span>\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> make_subplots\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\"> generate_html_report\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(top_movers, cannibalization, quick_wins, directory_analysis):\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"\"\"Génère un rapport HTML standalone avec graphiques Plotly intégrés.\"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Graphique top movers\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    tm \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> top_movers.head(\u003C/span>\u003Cspan style=\"color:#79B8FF\">20\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    fig_movers \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> go.Figure()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    fig_movers.add_trace(go.Bar(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        x\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">tm[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].apply(\u003C/span>\u003Cspan style=\"color:#F97583\">lambda\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> x: x.split(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)[\u003C/span>\u003Cspan style=\"color:#F97583\">-\u003C/span>\u003Cspan style=\"color:#79B8FF\">2\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> x.endswith(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">else\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> x.split(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)[\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:#FFAB70\">        y\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">tm[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks_delta'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">],\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        marker_color\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'#ef4444'\u003C/span>\u003Cspan style=\"color:#F97583\"> if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> v \u003C/span>\u003Cspan style=\"color:#F97583\">&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003Cspan style=\"color:#F97583\"> else\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '#22c55e'\u003C/span>\u003Cspan style=\"color:#F97583\"> for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> v \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> tm[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks_delta'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]],\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        text\u003C/span>\u003Cspan style=\"color:#F97583\">=\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\">v\u003C/span>\u003Cspan style=\"color:#F97583\">:+d\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> v \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> tm[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'clicks_delta'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]],\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        textposition\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'outside'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ))\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    fig_movers.update_layout(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        title\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Top 20 pages — Variation de clics WoW'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        xaxis_tickangle\u003C/span>\u003Cspan style=\"color:#F97583\">=-\u003C/span>\u003Cspan style=\"color:#79B8FF\">45\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        height\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\">500\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">        template\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'plotly_white'\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\">    # Assembler le HTML\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    report_html \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#F97583\"> f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    &#x3C;!DOCTYPE html>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    &#x3C;html lang=\"fr\">\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    &#x3C;head>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        &#x3C;meta charset=\"UTF-8\">\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        &#x3C;title>Rapport SEO Hebdomadaire — Semaine 20&#x3C;/title>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        &#x3C;script src=\"https://cdn.plot.ly/plotly-2.35.0.min.js\">&#x3C;/script>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        &#x3C;style>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            body \u003C/span>\u003Cspan style=\"color:#79B8FF\">{{\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> font-family: 'Inter', -apple-system, sans-serif; max-width: 1200px; margin: 0 auto; padding: 2rem; color: #1e293b; \u003C/span>\u003Cspan style=\"color:#79B8FF\">}}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            h1 \u003C/span>\u003Cspan style=\"color:#79B8FF\">{{\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> border-bottom: 3px solid #3b82f6; padding-bottom: 0.5rem; \u003C/span>\u003Cspan style=\"color:#79B8FF\">}}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            .metric-grid \u003C/span>\u003Cspan style=\"color:#79B8FF\">{{\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin: 2rem 0; \u003C/span>\u003Cspan style=\"color:#79B8FF\">}}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            .metric-card \u003C/span>\u003Cspan style=\"color:#79B8FF\">{{\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> background: #f8fafc; border-radius: 8px; padding: 1.5rem; text-align: center; \u003C/span>\u003Cspan style=\"color:#79B8FF\">}}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            .metric-value \u003C/span>\u003Cspan style=\"color:#79B8FF\">{{\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> font-size: 2rem; font-weight: 700; color: #1e293b; \u003C/span>\u003Cspan style=\"color:#79B8FF\">}}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            .metric-delta \u003C/span>\u003Cspan style=\"color:#79B8FF\">{{\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> font-size: 0.9rem; margin-top: 0.25rem; \u003C/span>\u003Cspan style=\"color:#79B8FF\">}}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            .metric-delta.positive \u003C/span>\u003Cspan style=\"color:#79B8FF\">{{\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> color: #22c55e; \u003C/span>\u003Cspan style=\"color:#79B8FF\">}}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            .metric-delta.negative \u003C/span>\u003Cspan style=\"color:#79B8FF\">{{\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> color: #ef4444; \u003C/span>\u003Cspan style=\"color:#79B8FF\">}}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            table \u003C/span>\u003Cspan style=\"color:#79B8FF\">{{\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> width: 100%; border-collapse: collapse; margin: 1rem 0; \u003C/span>\u003Cspan style=\"color:#79B8FF\">}}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            th, td \u003C/span>\u003Cspan style=\"color:#79B8FF\">{{\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> padding: 0.75rem; text-align: left; border-bottom: 1px solid #e2e8f0; \u003C/span>\u003Cspan style=\"color:#79B8FF\">}}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            th \u003C/span>\u003Cspan style=\"color:#79B8FF\">{{\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> background: #f1f5f9; font-weight: 600; \u003C/span>\u003Cspan style=\"color:#79B8FF\">}}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            .commentary \u003C/span>\u003Cspan style=\"color:#79B8FF\">{{\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> background: #eff6ff; border-left: 4px solid #3b82f6; padding: 1rem 1.5rem; margin: 1.5rem 0; border-radius: 0 4px 4px 0; \u003C/span>\u003Cspan style=\"color:#79B8FF\">}}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        &#x3C;/style>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    &#x3C;/head>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    &#x3C;body>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        &#x3C;h1>Rapport SEO — Semaine du 12 au 18 mai 2026&#x3C;/h1>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        &#x3C;p>Généré automatiquement le \u003C/span>\u003Cspan style=\"color:#79B8FF\">{\u003C/span>\u003Cspan style=\"color:#E1E4E8\">datetime.now().strftime(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'\u003C/span>\u003Cspan style=\"color:#79B8FF\">%d\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/%m/%Y à %H:%M'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\">&#x3C;/p>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        &#x3C;div class=\"metric-grid\">\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            &#x3C;div class=\"metric-card\">\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                &#x3C;div class=\"metric-label\">Clics totaux&#x3C;/div>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                &#x3C;div class=\"metric-value\">42,847&#x3C;/div>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                &#x3C;div class=\"metric-delta negative\">-3.2% vs S-1&#x3C;/div>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            &#x3C;/div>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            &#x3C;!-- ... autres métriques ... -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        &#x3C;/div>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        &#x3C;h2>Pages avec les plus fortes variations&#x3C;/h2>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        &#x3C;div id=\"chart-movers\">&#x3C;/div>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        &#x3C;script>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            var data = \u003C/span>\u003Cspan style=\"color:#79B8FF\">{\u003C/span>\u003Cspan style=\"color:#E1E4E8\">fig_movers.to_json()\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            Plotly.newPlot('chart-movers', data.data, data.layout);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        &#x3C;/script>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        &#x3C;div class=\"commentary\">\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            &#x3C;strong>Analyse automatique :&#x3C;/strong> 6 des 10 pages en baisse appartiennent au répertoire \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            /randonnee/. La position moyenne de ce répertoire est passée de 4.2 à 5.8. \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            Corrélation probable avec la perte des rich results FAQ sur ces pages.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        &#x3C;/div>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        &#x3C;h2>Opportunités Quick Win&#x3C;/h2>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        &#x3C;table>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            &#x3C;tr>&#x3C;th>Page&#x3C;/th>&#x3C;th>Position moy.&#x3C;/th>&#x3C;th>Impressions&#x3C;/th>&#x3C;th>CTR&#x3C;/th>&#x3C;th>CTR attendu&#x3C;/th>&#x3C;/tr>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            &#x3C;!-- Injecté dynamiquement -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        &#x3C;/table>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    &#x3C;/body>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    &#x3C;/html>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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:#F97583\">    with\u003C/span>\u003Cspan style=\"color:#79B8FF\"> open\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'rapport_seo_s20.html'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'w'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">encoding\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'utf-8'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">as\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> f:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        f.write(report_html)\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:#9ECBFF\"> 'rapport_seo_s20.html'\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>L'avantage du HTML standalone : partageable par email, stockable dans un Google Drive, consultable sans outil spécifique. Pour une conversion en PDF, un simple \u003Ccode>playwright pdf rapport_seo_s20.html rapport_seo_s20.pdf\u003C/code> fait le travail.\u003C/p>\n\u003Ch2>Automatiser les commentaires analytiques\u003C/h2>\n\u003Cp>Le reporting sans analyse est un dashboard. Ce qui fait la valeur d'un rapport stakeholders, ce sont les commentaires contextuels. C'est là que Claude Code apporte un avantage décisif par rapport à un script Python classique.\u003C/p>\n\u003Ch3>Faire rédiger l'analyse par Claude Code\u003C/h3>\n\u003Cp>Après avoir calculé les métriques, vous pouvez demander à Claude Code de générer les commentaires en langage naturel. La clé : fournir le contexte business dans votre prompt.\u003C/p>\n\u003Cp>Un exemple de workflow dans Claude Code :\u003C/p>\n\u003Col>\n\u003Cli>Le script Python s'exécute et génère les DataFrames d'analyse\u003C/li>\n\u003Cli>Vous demandez à Claude Code : \u003Cem>\"Analyse les résultats. Contexte : nous avons migré 47 URLs de /equipement/chaussures/ vers /chaussures-trail/ le 8 mai. Rédige un paragraphe de synthèse pour le comité de direction, en français, max 150 mots, focus sur l'impact business.\"\u003C/em>\u003C/li>\n\u003Cli>Claude Code produit : \u003Cem>\"La migration des 47 URLs chaussures, effective le 8 mai, montre des signaux encourageants à J+10. Les nouvelles URLs captent 78% du trafic des anciennes (1 240 clics vs 1 590 pré-migration). 3 URLs n'ont pas encore été réindexées — vérification des redirections 301 recommandée. La catégorie /chaussures-trail/ gagne 2 positions en moyenne sur les requêtes \"chaussures trail [marque]\". Projection à J+30 : retour au niveau pré-migration si les 3 URLs bloquées sont corrigées cette semaine.\"\u003C/em>\u003C/li>\n\u003C/ol>\n\u003Cp>Ce type de commentaire contextuel est impossible à automatiser avec Looker Studio ou Google Sheets. C'est du traitement du langage sur des données structurées, exactement ce pour quoi les LLM sont pertinents.\u003C/p>\n\u003Ch3>Edge case : quand l'analyse automatique se trompe\u003C/h3>\n\u003Cp>Un LLM peut sur-interpréter une corrélation. Si le trafic d'une catégorie baisse la semaine d'un long weekend, Claude Code pourrait attribuer ça à un problème SEO alors que c'est simplement saisonnier.\u003C/p>\n\u003Cp>Deux garde-fous :\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>Injectez les données de l'année précédente\u003C/strong> pour la même période. Si le pattern est similaire, c'est saisonnier.\u003C/li>\n\u003Cli>\u003Cstrong>Ajoutez dans votre prompt\u003C/strong> : \"Avant d'attribuer une cause, vérifie si la variation est cohérente avec la saisonnalité (données Y-1 fournies) et avec les variations du marché (données par device et par pays).\"\u003C/li>\n\u003C/ul>\n\u003Cp>Ne faites jamais confiance aveuglément à un commentaire auto-généré. Relisez-le, surtout avant de l'envoyer à votre C-level.\u003C/p>\n\u003Ch2>Intégrer des sources de données tierces\u003C/h2>\n\u003Cp>Le vrai potentiel de ce pipeline apparaît quand vous croisez les données GSC avec d'autres sources. Voici les croisements les plus utiles.\u003C/p>\n\u003Ch3>GSC + Screaming Frog : identifier les régressions techniques qui impactent le trafic\u003C/h3>\n\u003Cp>Exportez un crawl Screaming Frog en CSV (\u003Ccode>Internal_All.csv\u003C/code>) et croisez avec les données 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\">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:#6A737D\"># Charger l'export Screaming Frog\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">sf \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> pd.read_csv(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Internal_All.csv'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">usecols\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">[\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'Address'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Status Code'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Indexability'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Title 1'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'Meta Description 1'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'H1-1'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Canonical Link Element 1'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'Word Count'\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:#6A737D\"># Normaliser les URLs pour le join\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">sf[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> sf[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Address'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].str.rstrip(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">df_current[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page_clean'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> df_current[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].str.rstrip(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Croisement\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\"> df_current.merge(sf, \u003C/span>\u003Cspan style=\"color:#FFAB70\">left_on\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page_clean'\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\">'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\">'left'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Pages avec du trafic GSC mais un problème technique\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">issues \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> merged[\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:#79B8FF\"> 10\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">&#x26;\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\">'Indexability'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">==\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'Non-Indexable'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        (merged[\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\">|\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        (merged[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Canonical Link Element 1'\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\">'Address'\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\">\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\">\"ALERTE : \u003C/span>\u003Cspan style=\"color:#79B8FF\">{len\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(issues)\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> pages avec du trafic ont un problème technique\"\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\">(issues[[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page_clean'\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\">'Status Code'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Indexability'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Canonical Link Element 1'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]].to_string())\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Sur notre e-commerce outdoor de 12 000 pages, ce croisement a identifié 23 pages produit qui généraient encore 340 clics/semaine mais retournaient un 301 vers une page de catégorie générique — résultat d'une mise à jour de catalogue bâclée. 340 clics × taux de conversion moyen de 2.1% × panier moyen de 89€ = 640€ de revenu hebdomadaire en danger.\u003C/p>\n\u003Cp>Ce genre de croisement est exactement le type de régression qu'un outil comme Seogard détecte automatiquement et en continu, sans attendre le rapport hebdomadaire.\u003C/p>\n\u003Ch3>GSC + données AI Search\u003C/h3>\n\u003Cp>Avec l'expansion des liens dans les résultats AI de Google, il devient pertinent de croiser vos données GSC avec votre visibilité dans les AI Overviews. Si vous trackez les requêtes où vos pages apparaissent dans les \u003Ca href=\"/blog/google-s-ai-search-now-shows-more-links-what-seos-need-to-know-via-sejournal-mattgsouthern\">résultats de recherche AI\u003C/a>, vous pouvez comparer le CTR de ces requêtes avec celles qui déclenchent des résultats classiques.\u003C/p>\n\u003Cp>Le challenge actuel : Google ne fournit \u003Ca href=\"/blog/google-adds-more-ai-search-links-still-no-click-data-for-seos-via-sejournal-mattgsouthern\">toujours pas de données de clics spécifiques aux AI Overviews\u003C/a> dans la GSC. Vous devez donc utiliser des proxys — des outils tiers de SERP tracking pour identifier les requêtes qui déclenchent des AI Overviews, puis croiser avec vos données GSC.\u003C/p>\n\u003Ch2>Orchestrer le workflow : du one-shot au cron job\u003C/h2>\n\u003Cp>Claude Code est interactif par nature. Pour un workflow récurrent, vous avez deux options.\u003C/p>\n\u003Ch3>Option 1 : script Python autonome déclenché par cron\u003C/h3>\n\u003Cp>Vous utilisez Claude Code pour \u003Cstrong>développer\u003C/strong> le script, puis vous l'exécutez de manière autonome via cron ou un scheduler comme GitHub Actions :\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/seo-report.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 SEO Report\u003C/span>\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 7 * * 1'\u003C/span>\u003Cspan style=\"color:#6A737D\">  # Chaque lundi à 7h 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\"># Déclenchement manuel possible\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\">\u003Cspan style=\"color:#E1E4E8\">      \u003C/span>\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\">\u003Cspan style=\"color:#E1E4E8\">      \u003C/span>\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 -r requirements.txt\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>\u003Cspan style=\"color:#85E89D\">name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">Generate report\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\">          GSC_CREDENTIALS\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">${{ secrets.GSC_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\">python generate_report.py\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>\u003Cspan style=\"color:#85E89D\">name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">Send via email\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\">dawidd6/action-send-mail@v3\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\">          server_address\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">smtp.gmail.com\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          server_port\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">465\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          username\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">${{ secrets.MAIL_USER }}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          password\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">${{ secrets.MAIL_PASS }}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          subject\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"Rapport SEO Hebdomadaire — Semaine ${{ env.WEEK_NUMBER }}\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          to\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">seo-team@votresite.fr,direction@votresite.fr\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          from\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">reports@votresite.fr\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          attachments\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">rapport_seo_*.html\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch3>Option 2 : Claude Code en mode assisté pour l'analyse ad-hoc\u003C/h3>\n\u003Cp>Gardez le script de récupération des données en automatique, mais utilisez Claude Code manuellement pour l'analyse quand vous avez un contexte spécifique à injecter (migration en cours, core update Google, événement saisonnier).\u003C/p>\n\u003Cp>L'option 1 couvre 80% des besoins. L'option 2 est votre super-pouvoir pour les semaines atypiques — typiquement après un \u003Ca href=\"/blog/google-core-update-reshuffles-winners-ai-search-expands-links-seo-pulse-via-sejournal-mattgsouthern\">core update qui redistribue les cartes\u003C/a>.\u003C/p>\n\u003Ch2>Les limites à connaître avant de se lancer\u003C/h2>\n\u003Cp>Ce pipeline n'est pas sans friction. Quelques réalités à garder en tête :\u003C/p>\n\u003Cp>\u003Cstrong>Quota API GSC\u003C/strong> : la limite de 1 200 requêtes/minute est généreuse pour un seul site, mais si vous gérez un portefeuille de 20 domaines pour un grand compte, vous allez la toucher. Implémentez un rate limiter (\u003Ccode>time.sleep(0.1)\u003C/code> entre les appels suffit dans la plupart des cas).\u003C/p>\n\u003Cp>\u003Cstrong>Latence des données GSC\u003C/strong> : 3 jours minimum. Votre rapport du lundi reflète les données jusqu'à vendredi au mieux. Pour du monitoring en temps réel, la GSC ne suffit pas — vous avez besoin de données de crawl et de monitoring continu.\u003C/p>\n\u003Cp>\u003Cstrong>Coût Claude Code\u003C/strong> : chaque exécution de code consomme des tokens. Pour un rapport hebdomadaire standard, comptez environ 5 000-10 000 tokens par génération. Raisonnable, mais ça s'additionne si vous itérez beaucoup.\u003C/p>\n\u003Cp>\u003Cstrong>Reproductibilité\u003C/strong> : Claude Code peut générer un code légèrement différent à chaque exécution. Si la cohérence semaine-après-semaine est critique (et elle l'est pour un reporting C-level), figez le code généré dans un script Python versionné et ne refaites appel à Claude Code que pour les évolutions.\u003C/p>\n\u003Cp>\u003Cstrong>Sécurité\u003C/strong> : vos credentials OAuth ne doivent jamais être passées en clair dans un prompt Claude Code. Utilisez des variables d'environnement ou un secret manager. Le fichier \u003Ccode>token.json\u003C/code> généré par l'authentification OAuth donne un accès en lecture à toutes vos propriétés GSC — traitez-le comme un mot de passe.\u003C/p>\n\u003Ch2>Le pipeline en pratique : ce qui change dans votre semaine\u003C/h2>\n\u003Cp>Ce workflow remplace un processus de reporting fragile et chronophage par un pipeline reproductible. Les données GSC sont récupérées automatiquement, croisées avec vos exports techniques, analysées avec une logique métier que vous contrôlez, et présentées dans un format adapté à chaque audience.\u003C/p>\n\u003Cp>L'investissement initial est de 2-3 heures pour mettre en place le script, configurer l'authentification OAuth, et calibrer les seuils d'analyse. Le retour : chaque semaine, vous récupérez 2-3 heures de travail manuel, et surtout vous obtenez un rapport plus riche, plus rapide, et plus fiable que ce qu'un humain produirait en manipulant des CSV.\u003C/p>\n\u003Cp>Pour les régressions critiques entre deux rapports — une meta title qui disparaît, un canonical qui casse, un bloc de pages qui passe en noindex — un outil de monitoring continu comme Seogard reste indispensable. Le rapport hebdomadaire analyse les tendances. Le monitoring temps réel attrape les urgences.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,14,[18,19,20,21,22],"claude code","google search console","reporting SEO","automatisation","API GSC","Rapports SEO sur mesure avec Claude Code et Search Console","Tue May 19 2026 15:03:03 GMT+0000 (Coordinated Universal Time)",[26,42,56],{"_id":27,"slug":28,"__v":6,"author":7,"canonical":29,"category":10,"createdAt":30,"date":31,"description":32,"image":15,"imageAlt":15,"readingTime":33,"tags":34,"title":40,"updatedAt":41},"6a0d4ea4aa6b273b0cde629a","reasoning-lift-what-happens-to-brand-visibility-when-ai-thinks-harder","https://seogard.io/blog/reasoning-lift-what-happens-to-brand-visibility-when-ai-thinks-harder","2026-05-20T06:03:16.188Z","2026-05-20","Analyse technique de 200 réponses GPT-5.2 : le raisonnement élevé cite plus de sources, favorise le haut de funnel et redéfinit la visibilité de marque.",12,[35,36,37,38,39],"reasoning lift","AI search","brand visibility","GEO","LLM","Reasoning lift : impact du raisonnement IA sur la visibilité des marques","Wed May 20 2026 06:03:16 GMT+0000 (Coordinated Universal Time)",{"_id":43,"slug":44,"__v":6,"author":7,"canonical":45,"category":10,"createdAt":46,"date":31,"description":47,"image":15,"imageAlt":15,"readingTime":33,"tags":48,"title":54,"updatedAt":55},"6a0d86d3aa6b273b0c0cbec7","google-brings-ai-content-verification-to-search-via-sejournal-mattgsouthern","https://seogard.io/blog/google-brings-ai-content-verification-to-search-via-sejournal-mattgsouthern","2026-05-20T10:02:59.955Z","Google intègre SynthID à Search pour vérifier le contenu IA. Analyse technique des watermarks, impact sur le crawl et stratégies SEO concrètes.",[49,50,51,52,53],"google","synthid","ai content verification","search","seo technique","SynthID dans Search : impact technique sur le SEO","Wed May 20 2026 10:02:59 GMT+0000 (Coordinated Universal Time)",{"_id":57,"slug":58,"__v":6,"author":7,"canonical":59,"category":10,"createdAt":60,"date":31,"description":61,"image":15,"imageAlt":15,"readingTime":33,"tags":62,"title":68,"updatedAt":69},"6a0df755aa6b273b0c69952f","google-s-llms-txt-guidance-depends-on-which-product-you-ask-via-sejournal-mattgsouthern","https://seogard.io/blog/google-s-llms-txt-guidance-depends-on-which-product-you-ask-via-sejournal-mattgsouthern","2026-05-20T18:03:01.922Z","Google Search ignore llms.txt, mais Lighthouse l'audite pour l'agentic browsing. Analyse technique des contradictions et guide d'implémentation.",[63,64,65,66,67],"llms.txt","agentic browsing","Lighthouse","AI Search","Google","llms.txt : Google Search et Lighthouse se contredisent","Wed May 20 2026 18:03:01 GMT+0000 (Coordinated Universal Time)"]