Infinite scroll et SEO : guide technique d'implémentation

Le problème fondamental : Googlebot ne scrolle pas

Un catalogue e-commerce de 12 000 produits répartis sur 480 pages de listing. L'équipe produit déploie un infinite scroll pour améliorer l'engagement utilisateur. Trois semaines plus tard, 70 % des pages de listing ont disparu de l'index Google. Les produits au-delà de la 20e position sur chaque catégorie ne reçoivent plus aucune impression. Le trafic organique sur les pages catégories chute de 43 %.

Ce scénario n'est pas hypothétique. Il se produit chaque fois qu'une équipe implémente le défilement infini sans comprendre une réalité technique simple : Googlebot ne simule pas le scroll utilisateur. Le renderer de Google exécute le JavaScript, mais il ne déclenche pas les événements scroll, IntersectionObserver callbacks, ou les appels API attachés au défilement. Les contenus chargés dynamiquement au scroll restent invisibles au crawl.

La documentation officielle de Google sur l'infinite scroll est claire sur ce point : le moteur de recherche charge la page, exécute le JavaScript initial, mais n'interagit pas avec la page comme un utilisateur humain.

L'architecture qui fonctionne : pagination statique + infinite scroll en overlay

La seule approche robuste consiste à maintenir une pagination classique accessible au crawl, tout en offrant l'expérience d'infinite scroll aux utilisateurs. Les deux couches coexistent — la pagination est le squelette SEO, le défilement infini est l'habillage UX.

Le principe : des URLs de pagination réelles

Chaque "page" de résultats doit correspondre à une URL distincte, crawlable, avec son propre contenu HTML rendu côté serveur. L'infinite scroll se greffe par-dessus via JavaScript, en utilisant history.pushState pour mettre à jour l'URL au fil du défilement.

Prenons un site e-commerce avec une catégorie "chaussures-running" contenant 600 produits, affichés par 24 :

/chaussures-running          → produits 1-24
/chaussures-running?page=2   → produits 25-48
/chaussures-running?page=3   → produits 49-72
...
/chaussures-running?page=25  → produits 577-600

Chaque URL retourne du HTML complet (SSR ou pré-rendu). Le <noscript> ou le HTML initial contient les liens de pagination classiques <a href="?page=2">. Googlebot crawle ces liens, accède à chaque page, indexe l'intégralité du catalogue.

L'implémentation HTML côté serveur

Le markup initial de la page doit inclure la pagination complète, même si elle sera masquée visuellement par le JavaScript d'infinite scroll :

<!-- /chaussures-running?page=3 — rendu SSR -->
<main>
  <h1>Chaussures Running</h1>
  
  <div class="product-grid" data-page="3">
    <!-- 24 produits rendus côté serveur -->
    <article class="product-card">
      <a href="/chaussures-running/nike-pegasus-41">
        <img src="/img/nike-pegasus-41.webp" alt="Nike Pegasus 41" loading="lazy" />
        <h2>Nike Pegasus 41</h2>
        <span class="price">129,99 €</span>
      </a>
    </article>
    <!-- ... 23 autres produits -->
  </div>

  <!-- Pagination HTML — visible sans JS, cachée avec JS -->
  <nav class="pagination" aria-label="Pagination des résultats">
    <a href="/chaussures-running?page=2" rel="prev">Précédent</a>
    <a href="/chaussures-running">1</a>
    <a href="/chaussures-running?page=2">2</a>
    <span aria-current="page">3</span>
    <a href="/chaussures-running?page=4">4</a>
    <!-- ... -->
    <a href="/chaussures-running?page=25">25</a>
    <a href="/chaussures-running?page=4" rel="next">Suivant</a>
  </nav>
</main>

<!-- Balises head essentielles -->
<link rel="canonical" href="https://shop.example.com/chaussures-running?page=3" />

Points critiques dans ce markup :

  • Chaque page paginée a sa propre canonical pointant vers elle-même (pas vers la page 1). Google a déprécié rel="prev/next" en tant que signal d'indexation depuis 2019, mais les attributs rel="prev" et rel="next" sur les liens de navigation restent utiles pour l'accessibilité et certains crawlers tiers.
  • Les liens de pagination sont de vrais <a href>, pas des <button> ou des <span onclick>. Googlebot suit les <a href>. Il ignore les event handlers JavaScript.
  • Le contenu produit est rendu côté serveur dans le HTML initial. Pas de placeholder "loading..." attendant un appel API.

Le JavaScript d'infinite scroll avec pushState

Côté client, le JavaScript intercepte le scroll, charge les pages suivantes via fetch, les injecte dans le DOM, et met à jour l'URL :

// infinite-scroll.ts
class InfiniteScroll {
  private currentPage: number;
  private maxPage: number;
  private isLoading = false;
  private observer: IntersectionObserver;
  private productGrid: HTMLElement;
  private sentinel: HTMLElement;

  constructor() {
    this.productGrid = document.querySelector('.product-grid')!;
    this.currentPage = parseInt(this.productGrid.dataset.page || '1');
    this.maxPage = parseInt(this.productGrid.dataset.maxPage || '1');
    
    // Masquer la pagination HTML classique
    const pagination = document.querySelector('.pagination');
    if (pagination) {
      pagination.setAttribute('aria-hidden', 'true');
      (pagination as HTMLElement).style.display = 'none';
    }

    // Créer la sentinelle pour IntersectionObserver
    this.sentinel = document.createElement('div');
    this.sentinel.className = 'scroll-sentinel';
    this.productGrid.after(this.sentinel);

    this.observer = new IntersectionObserver(
      (entries) => this.handleIntersect(entries),
      { rootMargin: '400px' } // Précharger 400px avant d'atteindre le bas
    );
    this.observer.observe(this.sentinel);

    // Gérer le popstate pour la navigation back/forward
    window.addEventListener('popstate', (e) => this.handlePopState(e));
  }

  private async handleIntersect(entries: IntersectionObserverEntry[]) {
    const entry = entries[0];
    if (!entry.isIntersecting || this.isLoading || this.currentPage >= this.maxPage) return;

    this.isLoading = true;
    const nextPage = this.currentPage + 1;

    try {
      // Fetch la page suivante en HTML partiel
      const response = await fetch(
        `/api/products?category=${this.getCategory()}&page=${nextPage}`,
        { headers: { 'X-Requested-With': 'InfiniteScroll' } }
      );
      
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      
      const html = await response.text();
      
      // Injecter les produits avec un marqueur de page
      const pageSection = document.createElement('div');
      pageSection.dataset.page = String(nextPage);
      pageSection.className = 'product-page-section';
      pageSection.innerHTML = html;
      this.sentinel.before(pageSection);

      // Mettre à jour l'URL via pushState
      const newUrl = nextPage === 1 
        ? `/${this.getCategory()}`
        : `/${this.getCategory()}?page=${nextPage}`;
      
      history.pushState({ page: nextPage }, '', newUrl);
      this.currentPage = nextPage;

    } catch (error) {
      console.error('Infinite scroll fetch failed:', error);
      // Fallback : réafficher la pagination classique
      const pagination = document.querySelector('.pagination');
      if (pagination) {
        (pagination as HTMLElement).style.display = '';
        pagination.removeAttribute('aria-hidden');
      }
    } finally {
      this.isLoading = false;
    }
  }

  private handlePopState(event: PopStateEvent) {
    // Sur back/forward, recharger la page complète pour cohérence
    if (event.state?.page !== undefined) {
      window.location.reload();
    }
  }

  private getCategory(): string {
    return window.location.pathname.split('/').filter(Boolean)[0] || '';
  }
}

// Init uniquement si JS actif — la pagination HTML reste le fallback
if ('IntersectionObserver' in window) {
  new InfiniteScroll();
}

Plusieurs décisions techniques méritent attention ici :

Le header X-Requested-With permet au serveur de distinguer les requêtes d'infinite scroll (qui retournent un fragment HTML) des requêtes normales (qui retournent la page complète). Googlebot ne fera jamais ces requêtes AJAX — il accède à l'URL complète.

Le rootMargin: '400px' déclenche le chargement avant que l'utilisateur atteigne le bas. Ajustez cette valeur selon la latence de votre API. Pour un CDN avec des réponses < 100ms, 200px suffit. Pour une API à 300-500ms, montez à 600px.

Le fallback en cas d'erreur réaffiche la pagination classique. Un infinite scroll cassé ne doit jamais enfermer l'utilisateur sur une page sans navigation.

Le pushState est fondamental. Sans lui, l'URL reste sur /chaussures-running même après avoir scrollé jusqu'à la page 15. Si l'utilisateur partage l'URL ou fait un bookmark, il revient à la page 1. Et surtout, si pour une raison quelconque Google observait ces changements d'URL (via des signaux Chrome ou des rapports utilisateurs), il ne pourrait pas les associer aux bonnes pages.

Les pièges techniques qui tuent l'indexation

Le scroll-triggered fetch sans pagination HTML

Le pattern le plus courant — et le plus destructeur — est de n'avoir aucune pagination HTML, avec tout le catalogue accessible uniquement via des appels API déclenchés au scroll :

<!-- ❌ NE FAITES PAS ÇA -->
<div id="products">
  <!-- Seuls les 24 premiers produits sont dans le HTML -->
</div>
<script>
  // Tout le reste dépend du scroll → invisible pour Googlebot
  window.addEventListener('scroll', () => loadMore());
</script>

Googlebot voit 24 produits. Les 576 autres n'existent pas pour lui. Aucun lien interne ne pointe vers les pages de listing 2 à 25. Le maillage interne de votre catalogue s'effondre — un problème critique pour l'architecture de liens internes d'un e-commerce.

L'absence de self-canonical sur les pages paginées

Une erreur fréquente : toutes les pages de pagination portent une canonical vers la page 1.

Résultat : Google considère les pages 2+ comme des doublons et les désindexe. C'est une forme de soft 404 auto-infligée — le contenu existe, mais vous dites explicitement à Google de l'ignorer.

Chaque page paginée doit avoir <link rel="canonical" href="[sa propre URL]" />. La page /chaussures-running?page=7 porte une canonical vers /chaussures-running?page=7.

Le paramètre de page filtré par robots.txt ou meta noindex

Certaines configurations de navigation à facettes bloquent les paramètres d'URL par défaut. Si votre robots.txt contient Disallow: /*?page=, vous venez de bloquer toute la pagination. Vérifiez que vos règles de gestion des paramètres ne capturent pas la pagination dans leurs filets.

Le contenu dupliqué entre page initiale et pages paginées

Si /chaussures-running affiche les produits 1-24 ET /chaussures-running?page=1 affiche aussi les produits 1-24, vous avez deux URLs avec le même contenu. La page sans paramètre doit être la canonical, et ?page=1 doit porter une canonical vers /chaussures-running (sans le paramètre). Ou mieux : redirigez ?page=1 vers l'URL sans paramètre avec un 301.

# nginx.conf — rediriger ?page=1 vers l'URL propre
location ~ ^/([a-z-]+)$ {
    if ($arg_page = "1") {
        return 301 /$1;
    }
    # ... proxy vers votre application
}

Scénario réel : migration d'une pagination classique vers infinite scroll

Le contexte

Un site média (journalisme tech) avec 8 500 articles publiés. Les pages de listing (catégories, tags, archives mensuelles) représentent 340 URLs paginées qui génèrent 22 % du trafic organique — principalement des requêtes de type "actualités [sujet] [année]" et des requêtes longue traîne qui matchent les titres d'articles visibles sur les pages de listing.

L'équipe produit veut passer en infinite scroll sur toutes les pages de listing pour augmenter les pages vues par session (objectif pub). Le Lead SEO doit garantir zéro perte d'indexation.

L'audit pré-migration

Avant tout changement, l'audit avec Screaming Frog révèle :

  • 340 URLs de pagination indexées (vérifié via site:media-tech.example.com inurl:page)
  • 1 200 articles accessibles uniquement via les pages de listing 3+ (pas de lien interne depuis la homepage ou les articles liés)
  • Temps moyen de crawl des pages de listing : 180ms (SSR PHP, bien optimisé)
  • Budget crawl quotidien estimé : 2 800 pages/jour (l'analyse de logs montre que Googlebot visite environ 40 pages de listing par jour)

L'implémentation

L'équipe applique le pattern décrit plus haut : pagination HTML en SSR + infinite scroll en overlay JavaScript. Mais le site média a une contrainte supplémentaire : les listings sont ordonnés chronologiquement, et de nouveaux articles sont publiés quotidiennement. Cela signifie que le contenu de /actualites/ia?page=4 change chaque fois qu'un nouvel article pousse les anciens d'une position.

La solution : chaque page de listing inclut un sitemap dynamique qui reflète l'état actuel de la pagination. Le sitemap est mis à jour quotidiennement via un cron job, avec <lastmod> correct.

<!-- sitemap-listings.xml — généré dynamiquement -->
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://media-tech.example.com/actualites/ia</loc>
    <lastmod>2026-03-29</lastmod>
    <changefreq>daily</changefreq>
  </url>
  <url>
    <loc>https://media-tech.example.com/actualites/ia?page=2</loc>
    <lastmod>2026-03-29</lastmod>
    <changefreq>daily</changefreq>
  </url>
  <!-- ... toutes les pages paginées avec leur lastmod réel -->
  <url>
    <loc>https://media-tech.example.com/actualites/ia?page=14</loc>
    <lastmod>2026-03-22</lastmod>
    <changefreq>weekly</changefreq>
  </url>
</urlset>

Le monitoring post-déploiement

Dans les 48 heures suivant le déploiement, l'équipe surveille :

  1. Search Console > Indexation : le rapport "Pages" ne doit pas montrer de hausse des pages "Explorées, non indexées" pour les URLs paginées.
  2. Logs serveur : vérifier que Googlebot continue de crawler les URLs ?page=N. Un outil de monitoring comme Seogard détecte automatiquement si des pages précédemment indexées commencent à retourner des codes HTTP inattendus ou si des canonical disparaissent.
  3. Screaming Frog en mode JavaScript rendering : crawler le site en activant le rendu JS pour confirmer que le contenu visible en infinite scroll ne crée pas de contenu dupliqué avec les pages de pagination statiques.

Résultat après 4 semaines : les 340 URLs de pagination restent indexées. Le temps passé sur les pages de listing augmente de 35 % (objectif produit atteint). Le trafic organique sur les pages de listing reste stable (variation de -1,2 %, dans la marge de fluctuation normale).

Le cas particulier du "Load More" button

Une variante de l'infinite scroll est le bouton "Voir plus" / "Load More". L'utilisateur clique explicitement pour charger la suite. Du point de vue SEO, le problème est identique : si le bouton déclenche un fetch JavaScript sans URL accessible, Googlebot ne verra jamais le contenu supplémentaire.

La solution est la même : le bouton "Load More" doit être un vrai lien <a href> vers la page suivante, dont le comportement est intercepté par JavaScript pour charger le contenu inline :

<!-- Le "Load More" qui fonctionne pour le SEO -->
<a href="/chaussures-running?page=2" 
   class="load-more-btn" 
   data-page="2"
   aria-label="Charger les produits suivants">
  Voir plus de produits
</a>

<script>
document.querySelector('.load-more-btn')?.addEventListener('click', async (e) => {
  e.preventDefault();
  const btn = e.currentTarget as HTMLAnchorElement;
  const nextPage = btn.dataset.page;
  
  // Charger et injecter le contenu
  const res = await fetch(`/api/products?page=${nextPage}`, {
    headers: { 'X-Requested-With': 'LoadMore' }
  });
  const html = await res.text();
  document.querySelector('.product-grid')!.insertAdjacentHTML('beforeend', html);
  
  // Mettre à jour le bouton pour la page suivante
  const next = parseInt(nextPage!) + 1;
  btn.href = `/chaussures-running?page=${next}`;
  btn.dataset.page = String(next);
  
  // pushState
  history.pushState({ page: nextPage }, '', btn.href);
});
</script>

Le <a href> est la clé. Sans JavaScript (cas de Googlebot dans la majorité des crawls), c'est un lien classique vers la page suivante. Avec JavaScript, c'est un bouton interactif. Progressive enhancement dans sa forme la plus utile.

Validation technique et détection des régressions

Vérifier ce que Googlebot voit réellement

L'outil "Inspection d'URL" de Google Search Console permet de voir le HTML rendu pour une URL donnée. Testez systématiquement :

  • L'URL de la page 1 sans paramètre
  • Une URL de pagination intermédiaire (?page=5)
  • La dernière page de pagination

Pour chaque URL, vérifiez dans le HTML rendu que :

  • Les produits/articles de la page sont présents dans le DOM
  • La navigation de pagination avec les liens <a href> est visible
  • La balise canonical pointe vers la bonne URL
  • Aucun contenu de la page précédente ou suivante n'est injecté (pas de duplication)

Crawl automatisé avec Screaming Frog

Configurez un crawl Screaming Frog en deux passes :

  1. Mode HTML uniquement : vérifie que la pagination est accessible sans JavaScript. Tous les liens ?page=N doivent apparaître dans le crawl. Si des pages de pagination manquent, votre fallback HTML est cassé.

  2. Mode JavaScript rendering (Configuration > Spider > Rendering > JavaScript) : vérifie que l'infinite scroll ne crée pas d'URLs ou de contenu fantôme. Comparez les deux rapports — le nombre de pages découvertes et le contenu de chaque page doivent être cohérents.

Monitoring continu

Le danger de l'infinite scroll n'est pas seulement le déploiement initial — c'est la régression silencieuse. Un développeur modifie le composant de pagination lors d'un refactoring. Un nouveau déploiement casse le rendu SSR des pages ?page=N. Le CDN cache une version sans les liens de pagination.

Un monitoring continu est indispensable. Seogard, par exemple, détecte automatiquement les changements de canonical, les disparitions de liens internes dans le markup, et les variations de contenu rendu — exactement le type de régression qui peut passer inaperçue pendant des semaines sur un site avec des centaines de pages paginées.

Configurez des alertes spécifiques sur vos pages de listing les plus stratégiques. Un déploiement vendredi soir qui casse la pagination de votre top 10 catégories ne doit pas attendre lundi matin pour être détecté.

Performance et crawl budget : les considérations avancées

L'infinite scroll a un impact indirect sur le crawl budget. Chaque page de pagination est une URL que Googlebot doit crawler. Pour un site avec 50 catégories et une moyenne de 20 pages par catégorie, cela représente 1 000 URLs de pagination. C'est généralement négligeable par rapport au volume de pages produits ou articles.

Mais attention à deux scénarios problématiques :

Pages de pagination trop profondes. Si une catégorie a 200 pages de pagination, Googlebot atteindra-t-il la page 200 ? Probablement pas lors d'un crawl normal. La structure de votre site doit garantir que les pages de pagination profondes sont atteignables en un nombre raisonnable de clics depuis la racine. Le sitemap XML aide, mais ne remplace pas un maillage interne solide.

Temps de réponse des pages paginées. Si vos pages de listing nécessitent des requêtes SQL lourdes (tri, filtrage, comptage total), les pages à offset élevé (OFFSET 4800 LIMIT 24) deviennent lentes. La pagination par curseur (keyset pagination) est préférable pour maintenir des temps de réponse constants quelle que soit la profondeur :

-- ❌ Pagination par offset — O(n) avec la profondeur
SELECT * FROM products 
WHERE category_id = 42 
ORDER BY created_at DESC 
LIMIT 24 OFFSET 4800;

-- ✅ Pagination par curseur — temps constant
SELECT * FROM products 
WHERE category_id = 42 
  AND created_at < '2025-11-15T14:30:00Z'
ORDER BY created_at DESC 
LIMIT 24;

La pagination par curseur complique légèrement la génération d'URLs (le paramètre ?page=N devient un ?cursor=xxx), mais les gains de performance en valent la peine au-delà de 50-100 pages de profondeur. Vous pouvez maintenir des URLs ?page=N côté public et traduire en curseur côté base de données via un mapping en cache.

Le récapitulatif opérationnel

L'infinite scroll n'est pas incompatible avec le SEO — mais il ne peut pas être l'unique mécanisme de navigation. La pagination HTML statique reste le fondement de l'indexation. Le défilement infini n'est qu'une couche de progressive enhancement qui améliore l'UX sans remplacer l'architecture crawlable. Chaque page paginée doit exister en tant qu'URL autonome, avec son contenu rendu en SSR, sa propre canonical, et ses liens de navigation vers les pages adjacentes.

La clé de la pérennité de cette architecture, c'est le monitoring. Les régressions sur les pages paginées sont particulièrement insidieuses : elles affectent des centaines d'URLs d'un coup, mais ne se manifestent dans les métriques de trafic qu'avec 2-3 semaines de décalage — le temps que Google désindexe progressivement les pages cassées. Automatisez la détection pour réagir en heures, pas en semaines.

Articles connexes

Architecture29 mars 2026

URL structure : conventions SEO pour des URLs solides

Slugs, hiérarchie, trailing slashes, paramètres : guide technique complet pour structurer vos URLs sans piéger Googlebot ni votre crawl budget.

Architecture29 mars 2026

Mega menus et SEO : impact sur le crawl budget et le PageRank

Comment les méga-menus diluent le PageRank et gaspillent le crawl budget. Audit, correctifs techniques et patterns HTML pour reprendre le contrôle.

Architecture28 mars 2026

Architecture flat vs deep : profondeur de clic et crawl SEO

Flat ou deep structure ? Analysez l'impact de la profondeur de clic sur le crawl budget, le PageRank interne et l'indexation de vos pages.