Construire un plan de site dynamique pour 91 000 pages avec Next.js et Supabase

Le mois dernier, nous avons atteint 91 000 pages sur Deluxe Astrology. Des cartes de naissance de célébrités, des articles de blog, du contenu localisé dans six langues -- le site avait grandi bien au-delà de ce qu'un seul fichier sitemap pouvait gérer. Le protocole de sitemap de Google vous limite à 50 000 URL par fichier et 50 Mo non compressé. Nous avions besoin d'un index de sitemap avec des sous-sitemaps en chunks, tous générés dynamiquement depuis Supabase, mis en cache avec ISR sur Vercel, et soumis à Google Search Console comme une seule URL d'index.

Ceci est exactement l'implémentation que nous avons déployée. Pas une explication théorique -- du vrai code de production qui gère 91 000 URL aujourd'hui et qui s'adaptera à 500 000 sans changements.

Table des matières

Construire un plan de site dynamique pour 91 000 pages avec Next.js et Supabase

Comprendre les limites et l'architecture du sitemap

Voici les limites strictes que vous devez connaître :

Contrainte Limite Source
URL par fichier sitemap 50 000 protocole sitemaps.org
Taille du fichier par sitemap 50 Mo non compressé protocole sitemaps.org
Sitemaps par index de sitemap 50 000 protocole sitemaps.org
Max .range() de Supabase par requête 1 000 lignes (par défaut) Configuration Supabase PostgREST
Délai d'expiration de la fonction serverless Vercel (Pro) 60 secondes Docs Vercel 2025
Limite de taille du corps de réponse Vercel 10 Mo Mise en cache edge Vercel

Pour 91 000 URL, vous avez besoin d'un minimum de deux fichiers sitemap. Mais nous ne déversons pas tout dans deux buckets de 50 000 URL. Nous divisons par type de contenu -- célébrités, articles de blog, pages statiques, pages localisées -- parce que chaque type a différentes valeurs changefreq, priority et des patterns de mise à jour. Cela nous donne un meilleur contrôle et rend le débogage dans GSC beaucoup plus facile quand quelque chose ne va pas.

La structure du sitemap pour Deluxe Astrology

Voici à quoi ressemble l'architecture finale du sitemap :

/sitemap.xml                    → Index de sitemap (pointe vers tous les sous-sitemaps)
  /sitemap-pages.xml            → Pages statiques (~30 URL)
  /sitemap-blog-0.xml           → Chunk 0 d'articles de blog (jusqu'à 50 K)
  /sitemap-blog-1.xml           → Chunk 1 d'articles de blog (débordement)
  /sitemap-celebrities-0.xml    → Chunk 0 de pages de célébrités (jusqu'à 50 K)
  /sitemap-celebrities-1.xml    → Chunk 1 de pages de célébrités (débordement)
  /sitemap-locale-es.xml        → Pages localisées en espagnol
  /sitemap-locale-fr.xml        → Pages localisées en français
  /sitemap-locale-de.xml        → Pages localisées en allemand
  /sitemap-locale-pt.xml        → Pages localisées en portugais
  /sitemap-locale-ja.xml        → Pages localisées en japonais

Chaque sous-sitemap est un gestionnaire de route App Router de Next.js qui interroge Supabase à l'exécution, génère du XML, et met en cache via ISR avec revalidate = 3600 (toutes les heures). L'index du sitemap lui-même est également un gestionnaire de route.

Configurer les requêtes Supabase avec pagination par décalage

Voici le morceau critique que la plupart des tutoriels ratent : vous ne pouvez pas simplement faire supabase.from('celebrities').select('*') et vous attendre à récupérer 91 000 lignes en retour. La couche PostgREST de Supabase revient par défaut à un maximum de 1 000 lignes. Vous devez paginer.

Nous utilisons la pagination par décalage basée sur les plages par lots de 1 000 :

// lib/supabase-sitemap.ts
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY! // Utiliser la clé de rôle de service pour le côté serveur
);

const BATCH_SIZE = 1000;

export interface SitemapEntry {
  slug: string;
  updated_at: string;
}

export async function fetchAllSlugs(
  table: string,
  selectColumns: string = 'slug, updated_at'
): Promise<SitemapEntry[]> {
  const allRows: SitemapEntry[] = [];
  let offset = 0;
  let hasMore = true;

  while (hasMore) {
    const { data, error } = await supabase
      .from(table)
      .select(selectColumns)
      .order('updated_at', { ascending: false })
      .range(offset, offset + BATCH_SIZE - 1);

    if (error) {
      console.error(`Erreur de récupération du sitemap pour ${table}:`, error.message);
      break;
    }

    if (data && data.length > 0) {
      allRows.push(...data);
      offset += BATCH_SIZE;
      hasMore = data.length === BATCH_SIZE;
    } else {
      hasMore = false;
    }
  }

  return allRows;
}

export async function fetchSlugsChunked(
  table: string,
  chunkIndex: number,
  chunkSize: number = 50000
): Promise<{ entries: SitemapEntry[]; totalCount: number }> {
  // D'abord obtenir le nombre total pour l'index du sitemap
  const { count } = await supabase
    .from(table)
    .select('*', { count: 'exact', head: true });

  const totalCount = count || 0;
  const startOffset = chunkIndex * chunkSize;
  const entries: SitemapEntry[] = [];
  let offset = startOffset;
  const endOffset = Math.min(startOffset + chunkSize, totalCount);

  while (offset < endOffset) {
    const batchEnd = Math.min(offset + BATCH_SIZE - 1, endOffset - 1);
    const { data, error } = await supabase
      .from(table)
      .select('slug, updated_at')
      .order('updated_at', { ascending: false })
      .range(offset, batchEnd);

    if (error || !data || data.length === 0) break;
    entries.push(...data);
    offset += data.length;
  }

  return { entries, totalCount };
}

export function getChunkCount(totalCount: number, chunkSize: number = 50000): number {
  return Math.ceil(totalCount / chunkSize);
}

Quelques points à noter ici. Nous utilisons la SUPABASE_SERVICE_ROLE_KEY -- pas la clé anon -- parce que ces gestionnaires de route s'exécutent côté serveur et nous ne voulons pas que les politiques RLS ralentissent nos requêtes de sitemap. La fonction fetchSlugsChunked ne récupère que le chunk spécifique nécessaire pour un fichier sitemap donné, pas l'ensemble des données. C'est important quand vous exécutez sur le délai d'expiration de 60 secondes de la fonction serverless de Vercel.

Construire un plan de site dynamique pour 91 000 pages avec Next.js et Supabase - architecture

Construire la route de l'index du sitemap

L'index du sitemap est l'URL unique que vous soumettez à Google. Il référence tous vos sous-sitemaps.

// app/sitemap.xml/route.ts
import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';

export const revalidate = 3600; // ISR : régénérer toutes les heures

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

const CHUNK_SIZE = 50000;
const SITE_URL = 'https://deluxeastrology.com';
const LOCALES = ['es', 'fr', 'de', 'pt', 'ja'];

async function getTableCount(table: string): Promise<number> {
  const { count } = await supabase
    .from(table)
    .select('*', { count: 'exact', head: true });
  return count || 0;
}

export async function GET() {
  const blogCount = await getTableCount('blog_posts');
  const celebrityCount = await getTableCount('celebrities');

  const blogChunks = Math.ceil(blogCount / CHUNK_SIZE);
  const celebrityChunks = Math.ceil(celebrityCount / CHUNK_SIZE);

  const now = new Date().toISOString();

  let sitemaps = '';

  // Sitemap des pages statiques
  sitemaps += `
    <sitemap>
      <loc>${SITE_URL}/sitemap-pages.xml</loc>
      <lastmod>${now}</lastmod>
    </sitemap>`;

  // Sitemaps de blog
  for (let i = 0; i < blogChunks; i++) {
    sitemaps += `
    <sitemap>
      <loc>${SITE_URL}/sitemap-blog-${i}.xml</loc>
      <lastmod>${now}</lastmod>
    </sitemap>`;
  }

  // Sitemaps de célébrités
  for (let i = 0; i < celebrityChunks; i++) {
    sitemaps += `
    <sitemap>
      <loc>${SITE_URL}/sitemap-celebrities-${i}.xml</loc>
      <lastmod>${now}</lastmod>
    </sitemap>`;
  }

  // Sitemaps localisés
  for (const locale of LOCALES) {
    sitemaps += `
    <sitemap>
      <loc>${SITE_URL}/sitemap-locale-${locale}.xml</loc>
      <lastmod>${now}</lastmod>
    </sitemap>`;
  }

  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${sitemaps}
</sitemapindex>`;

  return new NextResponse(xml, {
    headers: {
      'Content-Type': 'application/xml',
      'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=600',
    },
  });
}

Notez que nous ne faisons ici que des requêtes de count -- head: true signifie que Supabase retourne juste le nombre sans aucune donnée de ligne. Cela rend la génération de l'index du sitemap presque instantanée.

Construire des sitemaps individuels en chunks

Voici le gestionnaire du sitemap de célébrités avec la pagination complète :

// app/sitemap-celebrities-[chunk].xml/route.ts
import { NextResponse } from 'next/server';
import { fetchSlugsChunked } from '@/lib/supabase-sitemap';

export const revalidate = 3600;

const SITE_URL = 'https://deluxeastrology.com';

export async function GET(
  request: Request,
  { params }: { params: Promise<{ chunk: string }> }
) {
  const { chunk } = await params;
  const chunkIndex = parseInt(chunk, 10);

  if (isNaN(chunkIndex) || chunkIndex < 0) {
    return new NextResponse('Index de chunk invalide', { status: 400 });
  }

  const { entries } = await fetchSlugsChunked('celebrities', chunkIndex);

  const urls = entries
    .map(
      (entry) => `
  <url>
    <loc>${SITE_URL}/celebrities/${entry.slug}</loc>
    <lastmod>${new Date(entry.updated_at).toISOString()}</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.6</priority>
  </url>`
    )
    .join('');

  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls}
</urlset>`;

  return new NextResponse(xml, {
    headers: {
      'Content-Type': 'application/xml',
      'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=600',
    },
  });
}

Le sitemap de blog suit le même modèle mais avec une priorité et une fréquence de modification différentes :

// app/sitemap-blog-[chunk].xml/route.ts
import { NextResponse } from 'next/server';
import { fetchSlugsChunked } from '@/lib/supabase-sitemap';

export const revalidate = 3600;

const SITE_URL = 'https://deluxeastrology.com';

export async function GET(
  request: Request,
  { params }: { params: Promise<{ chunk: string }> }
) {
  const { chunk } = await params;
  const chunkIndex = parseInt(chunk, 10);

  const { entries } = await fetchSlugsChunked('blog_posts', chunkIndex);

  const urls = entries
    .map(
      (entry) => `
  <url>
    <loc>${SITE_URL}/blog/${entry.slug}</loc>
    <lastmod>${new Date(entry.updated_at).toISOString()}</lastmod>
    <changefreq>weekly</changefreq>
    <priority>0.8</priority>
  </url>`
    )
    .join('');

  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls}
</urlset>`;

  return new NextResponse(xml, {
    headers: {
      'Content-Type': 'application/xml',
      'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=600',
    },
  });
}

Vous devrez configurer le routage de Next.js pour gérer le segment dynamique. Dans App Router, le nom du dossier utilise des crochets :

app/
  sitemap.xml/
    route.ts
  sitemap-pages.xml/
    route.ts
  sitemap-blog-[chunk].xml/
    route.ts
  sitemap-celebrities-[chunk].xml/
    route.ts
  sitemap-locale-[lang].xml/
    route.ts

Si l'approche avec des crochets dans le nom du dossier vous pose problème avec votre système de fichiers ou votre IDE (cela arrive parfois), utilisez plutôt les réécriture de routes dans next.config.ts :

// next.config.ts
const nextConfig = {
  async rewrites() {
    return [
      {
        source: '/sitemap-blog-:chunk(\\d+).xml',
        destination: '/api/sitemap-blog/:chunk',
      },
      {
        source: '/sitemap-celebrities-:chunk(\\d+).xml',
        destination: '/api/sitemap-celebrities/:chunk',
      },
      {
        source: '/sitemap-locale-:lang.xml',
        destination: '/api/sitemap-locale/:lang',
      },
    ];
  },
};

export default nextConfig;

Sitemap des pages statiques

Pour le sitemap des pages statiques, nous codons en dur les URL car elles changent rarement :

// app/sitemap-pages.xml/route.ts
import { NextResponse } from 'next/server';

export const revalidate = 86400; // Une fois par jour est bon pour les pages statiques

const SITE_URL = 'https://deluxeastrology.com';

const staticPages = [
  { path: '/', priority: '1.0', changefreq: 'daily' },
  { path: '/about', priority: '0.7', changefreq: 'monthly' },
  { path: '/solutions/birth-chart', priority: '0.9', changefreq: 'weekly' },
  { path: '/solutions/compatibility', priority: '0.9', changefreq: 'weekly' },
  { path: '/solutions/transit-report', priority: '0.9', changefreq: 'weekly' },
  { path: '/blog', priority: '0.8', changefreq: 'daily' },
  { path: '/celebrities', priority: '0.8', changefreq: 'daily' },
  { path: '/contact', priority: '0.5', changefreq: 'yearly' },
  { path: '/pricing', priority: '0.7', changefreq: 'monthly' },
];

export async function GET() {
  const now = new Date().toISOString();

  const urls = staticPages
    .map(
      (page) => `
  <url>
    <loc>${SITE_URL}${page.path}</loc>
    <lastmod>${now}</lastmod>
    <changefreq>${page.changefreq}</changefreq>
    <priority>${page.priority}</priority>
  </url>`
    )
    .join('');

  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls}
</urlset>`;

  return new NextResponse(xml, {
    headers: {
      'Content-Type': 'application/xml',
      'Cache-Control': 'public, s-maxage=86400, stale-while-revalidate=3600',
    },
  });
}

Sitemaps localisés avec Hreflang

C'est là que ça devient intéressant. Pour le contenu multilingue, vous avez besoin d'éléments xhtml:link avec des attributs hreflang. Chaque sitemap localisé référence toutes les versions en langage alternatif de chaque page :

// app/sitemap-locale-[lang].xml/route.ts
import { NextResponse } from 'next/server';
import { fetchAllSlugs } from '@/lib/supabase-sitemap';

export const revalidate = 3600;

const SITE_URL = 'https://deluxeastrology.com';
const ALL_LOCALES = ['en', 'es', 'fr', 'de', 'pt', 'ja'];

export async function GET(
  request: Request,
  { params }: { params: Promise<{ lang: string }> }
) {
  const { lang } = await params;

  if (!ALL_LOCALES.includes(lang)) {
    return new NextResponse('Locale invalide', { status: 404 });
  }

  const entries = await fetchAllSlugs('localized_pages');
  // Filtrer pour les pages ayant cette locale
  const localeEntries = entries.filter((e: any) => e.locale === lang);

  const urls = localeEntries
    .map((entry: any) => {
      const alternates = ALL_LOCALES.map(
        (loc) =>
          `    <xhtml:link rel="alternate" hreflang="${loc}" href="${SITE_URL}/${loc}/${entry.slug}" />`
      ).join('\n');

      return `
  <url>
    <loc>${SITE_URL}/${lang}/${entry.slug}</loc>
    <lastmod>${new Date(entry.updated_at).toISOString()}</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.6</priority>
${alternates}
  </url>`;
    })
    .join('');

  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:xhtml="http://www.w3.org/1999/xhtml">${urls}
</urlset>`;

  return new NextResponse(xml, {
    headers: {
      'Content-Type': 'application/xml',
      'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=600',
    },
  });
}

Stratégie de revalidation ISR

Nous définissons revalidate = 3600 sur toutes les routes de sitemap. Cela signifie que Vercel sert le XML en cache pendant jusqu'à une heure, puis le régénère en arrière-plan à la prochaine requête. Pour 91 000 pages, c'est le sweet spot -- assez fréquent pour que le nouveau contenu apparaisse le même jour, mais pas tellement agressif que nous martelons Supabase.

Pour la revalidation à la demande quand du contenu est publié, ajoutez un point de terminaison de revalidation :

// app/api/revalidate-sitemap/route.ts
import { revalidatePath } from 'next/cache';
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const { secret, paths } = await request.json();

  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
  }

  // Revalider les chemins spécifiques du sitemap
  const targetPaths = paths || ['/sitemap.xml'];
  for (const path of targetPaths) {
    revalidatePath(path);
  }

  return NextResponse.json({ revalidated: true, paths: targetPaths });
}

Ensuite, configurez un Webhook de base de données Supabase (ou un déclencheur Postgres via pg_net) pour appeler ce point de terminaison chaque fois que vos tables celebrities ou blog_posts sont mises à jour.

Priorité et fréquence de modification par type de contenu

Voici la matrice de priorité que nous utilisons. Google a dit qu'il ignore principalement priority et changefreq, mais d'autres moteurs de recherche (Bing, Yandex) les utilisent toujours, et cela ne fait pas de mal :

Type de contenu Priorité Fréquence de modification Justification
Page d'accueil 1.0 daily Importance maximale, mise à jour fréquente
Solutions/Fonctionnalités 0.9 weekly Pages de produit principal
Listing de blog 0.8 daily Nouveaux articles régulièrement
Articles de blog 0.8 weekly Contenu mis à jour occasionnellement
Pages de célébrités 0.6 monthly Change rarement après création
Pages localisées 0.6 monthly Les mises à jour de traduction sont peu fréquentes
Contact/Légal 0.5 yearly Presque jamais changé

La valeur lastmod est critique et doit toujours provenir de la colonne updated_at de votre base de données -- ne la codez jamais en dur en new Date(). Google utilise lastmod pour prioriser le recrawling, et si chaque page dit qu'elle a été modifiée maintenant, Google finira par ignorer entièrement votre lastmod.

Soumission à Google Search Console

C'est la partie simple. Dans GSC :

  1. Allez à Sitemaps dans la barre latérale gauche
  2. Entrez https://yourdomain.com/sitemap.xml (l'URL d'index uniquement)
  3. Cliquez sur Soumettre

C'est tout. Ne soumettez pas les sous-sitemaps individuels. Google lit l'index et découvre automatiquement tous les enfants. Vous devriez voir le statut "Succès" dans quelques heures, et les nombres d'URL indexées augmenteront au cours des 2-4 prochaines semaines.

Pour 91 000 URL, attendez-vous à ce que Google en indexe 70-90 % dans le premier mois. Les pages restantes ont généralement du contenu mince, des problèmes de contenu dupliqué, ou sont simplement une faible priorité dans l'allocation du budget de crawl de Google.

Ajoutez également votre sitemap à robots.txt :

# robots.txt
User-agent: *
Allow: /

Sitemap: https://deluxeastrology.com/sitemap.xml

Déboguer quand Google ne veut pas indexer vos pages

C'est là que la plupart des gens se bloquent. Vous avez soumis 91 000 URL mais GSC n'en affiche que 40 000 indexées. Voici la liste de contrôle systématique de débogage que nous suivons :

Vérifier les balises Noindex accidentelles

C'est la cause #1. Faites un contrôle ponctuel :

curl -s https://deluxeastrology.com/celebrities/some-slug | grep -i 'noindex'

Vérifiez également vos métadonnées de layout ou de page Next.js. Une erreur courante est de définir noindex dans un layout qui s'applique à des milliers de pages :

// MAUVAIS : Ceci noindexe toutes les pages utilisant ce layout
export const metadata = {
  robots: { index: false, follow: true },
};

Vérifier que robots.txt ne bloque pas le crawling

Vérifiez https://yourdomain.com/robots.txt dans un navigateur. Assurez-vous que vous ne bloquez pas accidentellement vos routes dynamiques. Sur Vercel, vérifiez également les middlewares qui pourraient retourner 403 à Googlebot.

Inspecter les erreurs de crawl dans GSC

Allez à PagesPourquoi les pages ne sont pas indexées. Les problèmes courants :

  • "Crawlé - actuellement non indexé" : Google a vu la page mais a décidé de ne pas l'indexer. Habituellement du contenu mince.
  • "Découvert - actuellement non indexé" : Google connaît l'URL mais ne l'a pas encore crawlée. Problème de budget de crawl.
  • "Exclu par balise noindex" : Évident. Corrigez la balise.
  • "Doublon sans canonique" : Ajoutez les balises canoniques appropriées.

Corriger les pages orphelines avec les liens internes

C'est énorme pour les sites volumineux. Si vos pages de célébrités ne sont découvrables que via le sitemap et n'ont zéro lien interne pointant vers elles, Google déprioritisera leur crawling. Ajoutez :

  • Des pages de catégorie/listing qui lient à des groupes de pages de célébrités
  • Des liens de célébrités associés sur chaque page de célébrité
  • Des sections "Tendance" ou "Récemment mise à jour" sur les pages à fort trafic
  • Navigation avec fil d'Ariane et données structurées

Valider les URL individuelles

Utilisez l'outil Inspection d'URL de GSC sur des pages spécifiques qui ne sont pas indexées. Il vous montre exactement ce que Google voit -- le HTML rendu, toute erreur, les problèmes d'utilisabilité mobile, et le statut d'indexation.

Vérifier les en-têtes de réponse du sitemap

Assurez-vous que vos routes de sitemap retournent les bons en-têtes :

curl -I https://deluxeastrology.com/sitemap.xml

Vous devriez voir Content-Type: application/xml et un statut 200. Si vous obtenez des réponses 304 Not Modified depuis les caches obsolètes, cela peut faire que Google saute la relecture de votre sitemap.

Benchmarks de performance et de coût

Voici les chiffres réels de notre déploiement en production au début de 2025 :

Métrique Valeur
URL totales dans le sitemap 91 247
Temps de génération de l'index du sitemap ~120 ms (requêtes de count uniquement)
Génération de sitemap individuel (50 000 URL) ~4,2 secondes
Coût de requête Supabase par régénération du sitemap ~0,01 $
Taille totale du XML du sitemap (tous fichiers) ~8,4 Mo non compressé
Bande passante Vercel pour les sitemaps par mois ~2,1 Go (principalement Googlebot)
Coût du plan Vercel Pro 20 $/utilisateur/mois
Coût du plan Supabase Pro 25 $/mois
Taux d'indexation GSC après 30 jours 84 % des URL soumises
Temps entre la publication du contenu et la mise à jour du sitemap ≤1 heure (ISR) ou ~5 secondes (à la demande)

Le grand enseignement : cette configuration entière coûte pratiquement rien à exécuter. La génération de sitemap est une erreur d'arrondi sur vos factures Vercel et Supabase.

Si vous construisez un projet similaire à grande échelle et souhaitez de l'aide avec l'architecture, nous avons fait cela sur plusieurs sites clients. Consultez nos capacités de développement Next.js ou notre travail de développement CMS headless. Pour les sites basés sur Astro avec des exigences d'échelle similaires, nous avons construit des solutions comparables en utilisant l'approche d'endpoint Astro.

Le code complet est disponible comme gist GitHub : tous les gestionnaires de route, la bibliothèque de requête Supabase, et les réécriture de routes next.config.ts. Si votre projet a besoin de quelque chose de plus personnalisé -- des sitemaps multi-locataires, une revalidation en temps réel, ou des sitemaps pour 1 million+ de pages -- nous contacter et nous délimiterons le périmètre.

FAQ

Combien d'URL un seul fichier sitemap peut-il contenir ?

Le protocole de sitemap permet un maximum de 50 000 URL par fichier et une taille de fichier non compressée de 50 Mo. Pour les sites avec plus de 50 000 pages, vous avez besoin d'un index de sitemap qui référence plusieurs fichiers sitemap en chunks. En pratique, la plupart des générateurs de sitemap chunkent à 45 000-50 000 URL pour laisser une marge de sécurité.

Dois-je utiliser next-sitemap ou construire des gestionnaires de route personnalisés ?

next-sitemap (v4+) est excellent pour les configurations plus simples et gère bien le chunking automatique. Mais pour 91 000+ pages dynamiques avec des priorités spécifiques au type de contenu, des sitemaps localisés avec hreflang, et un contrôle ISR fin, les gestionnaires de route personnalisés vous donnent plus de contrôle. Nous avons choisi personnalisé parce que nous avions besoin d'intervalles de revalidation différents par type de contenu et que nous voulions que la structure du sitemap corresponde à notre workflow de débogage GSC.

Dois-je soumettre chaque fichier de sitemap individuel à Google Search Console ?

Non. Soumettez uniquement l'URL de l'index du sitemap (par ex., https://yourdomain.com/sitemap.xml). Google lit l'index et découvre et traite automatiquement tous les sous-sitemaps référencés. Soumettre les fichiers individuels est inutile et encombre votre tableau de bord GSC.

À quelle fréquence les sitemaps doivent-ils être régénérés pour les gros sites dynamiques ?

Pour la plupart des sites riches en contenu, une régénération horaire via ISR (revalidate = 3600) est une bonne valeur par défaut. Si vous publiez du contenu très fréquemment, associez-la à une revalidation à la demande déclenchée par des webhooks de base de données. Ne régénérez pas à chaque requête -- cela contredit la mise en cache et augmente inutilement la charge de Supabase.

Pourquoi Google n'indexe pas toutes mes URL de sitemap ?

Les causes les plus courantes sont : les balises meta noindex accidentelles, robots.txt bloquant, contenu mince/dupliqué, pages orphelines sans liens internes, et limitations du budget de crawl. Vérifiez le rapport "Pages" de GSC sous "Pourquoi les pages ne sont pas indexées" pour des raisons spécifiques. Pour les gros sites, concentrez-vous sur l'amélioration des liens internes vers les pages orphelines -- c'est souvent le plus grand levier.

La valeur de priority dans les sitemaps affecte-t-elle réellement les classements Google ? Google a déclaré publiquement qu'il ignore largement les valeurs priority et changefreq. Cependant, Bing et les autres moteurs de recherche les utilisent. Le champ lastmod est le signal de sitemap le plus important -- assurez-vous qu'il reflète les changements réels de contenu de votre base de données, pas l'horodatage actuel.

Comment gérer la limite de 1 000 lignes de Supabase pour les requêtes de sitemap ?

Utilisez la méthode .range(offset, offset + batchSize - 1) de Supabase pour paginer par lots de 1 000. Bouclez jusqu'à ce que vous ayez récupéré toutes les lignes pour le chunk de sitemap actuel. Pour les requêtes de count uniquement (utilisées dans l'index du sitemap), utilisez .select('*', { count: 'exact', head: true }) qui retourne juste le count sans transférer aucune donnée de ligne.

Cette approche peut-elle gérer 500 000 ou 1 million de pages ?

Oui, avec des ajustements mineurs. L'architecture chunked s'adapte linéairement -- 1 million de pages produirait environ 20 sous-sitemaps. La principale préoccupation devient le délai d'expiration de 60 secondes de la fonction Vercel pour générer des sitemaps individuels de 50 000 URL. Si vous atteignez cette limite, réduisez la taille du chunk à 25 000 ou 10 000 URL par fichier. Le protocole de sitemap permet jusqu'à 50 000 sitemaps dans un index unique, donc vous n'atteindrez pas les limites de niveau index de sitôt.