Traducir un artículo en markdown al español

El mes pasado alcanzamos 91,000 páginas en Deluxe Astrology. Cartas natales de celebridades, publicaciones de blog, contenido localizado en seis idiomas -- el sitio había crecido mucho más allá de lo que un único archivo de mapa del sitio podía manejar. El protocolo de mapa del sitio de Google te limita a 50,000 URLs por archivo y 50MB sin comprimir. Necesitábamos un índice de mapa del sitio con sub-mapas chunked, todos generados dinámicamente desde Supabase, cacheados con ISR en Vercel, y enviados a Google Search Console como una única URL de índice.

Esta es la implementación exacta que lanzamos. No es un recorrido teórico -- código de producción real que maneja 91K URLs hoy y se escalará a 500K sin cambios.

Tabla de Contenidos

Building a Dynamic Sitemap for 91,000 Pages with Next.js and Supabase

Entender los Límites y la Arquitectura del Mapa del Sitio

Aquí están los límites difíciles que necesitas saber:

Restricción Límite Fuente
URLs por archivo de mapa del sitio 50,000 protocolo sitemaps.org
Tamaño de archivo por mapa del sitio 50MB sin comprimir protocolo sitemaps.org
Mapas del sitio por índice de mapa del sitio 50,000 protocolo sitemaps.org
Máximo de .range() de Supabase por consulta 1,000 filas (predeterminado) Configuración PostgREST de Supabase
Tiempo de espera de función serverless de Vercel (Pro) 60 segundos Docs de Vercel 2025
Límite de tamaño de cuerpo de respuesta de Vercel 10MB Caché de borde de Vercel

Para 91,000 URLs, necesitas como mínimo dos archivos de mapa del sitio. Pero no solo volcamos todo en dos cubos de 50K URL. Dividimos por tipo de contenido -- celebridades, publicaciones de blog, páginas estáticas, páginas localizadas -- porque cada tipo tiene diferentes changefreq, priority, y patrones de actualización. Esto nos da mejor control y hace que la depuración en GSC sea mucho más fácil cuando algo sale mal.

La Estructura del Mapa del Sitio para Deluxe Astrology

Así es como se ve la arquitectura final del mapa del sitio:

/sitemap.xml                    → Índice de Mapa del Sitio (apunta a todos los sub-mapas)
  /sitemap-pages.xml            → Páginas estáticas (~30 URLs)
  /sitemap-blog-0.xml           → Publicaciones de blog chunk 0 (hasta 50K)
  /sitemap-blog-1.xml           → Publicaciones de blog chunk 1 (desbordamiento)
  /sitemap-celebrities-0.xml    → Páginas de celebridades chunk 0 (hasta 50K)
  /sitemap-celebrities-1.xml    → Páginas de celebridades chunk 1 (desbordamiento)
  /sitemap-locale-es.xml        → Páginas localizadas en español
  /sitemap-locale-fr.xml        → Páginas localizadas en francés
  /sitemap-locale-de.xml        → Páginas localizadas en alemán
  /sitemap-locale-pt.xml        → Páginas localizadas en portugués
  /sitemap-locale-ja.xml        → Páginas localizadas en japonés

Cada sub-mapa del sitio es un manejador de ruta del App Router de Next.js que consulta Supabase en tiempo de ejecución, genera XML, y cachea mediante ISR con revalidate = 3600 (cada hora). El índice del mapa del sitio en sí también es un manejador de ruta.

Configurar Consultas de Supabase con Paginación de Offset

Aquí está la pieza crítica que la mayoría de los tutoriales hacen mal: no puedes simplemente hacer supabase.from('celebrities').select('*') y esperar 91,000 filas de vuelta. La capa PostgREST de Supabase por defecto devuelve máximo 1,000 filas. Necesitas paginar.

Usamos paginación de offset basada en rangos en lotes 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! // Usa la clave de rol de servicio para el servidor
);

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(`Error de obtención de mapa del sitio para ${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 }> {
  // Primero obtén el recuento total para el índice del mapa del sitio
  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);
}

Hay algunas cosas a tener en cuenta aquí. Usamos la SUPABASE_SERVICE_ROLE_KEY -- no la clave anon -- porque estos manejadores de ruta se ejecutan del lado del servidor y no queremos que las políticas RLS ralenticen nuestras consultas del mapa del sitio. La función fetchSlugsChunked solo obtiene el chunk específico necesario para un archivo de mapa del sitio dado, no todo el conjunto de datos. Eso importa cuando estás ejecutando en el tiempo de espera de función serverless de 60 segundos de Vercel.

Building a Dynamic Sitemap for 91,000 Pages with Next.js and Supabase - architecture

Construir la Ruta del Índice de Mapa del Sitio

El índice del mapa del sitio es la única URL que envías a Google. Referencia todos tus sub-mapas del sitio.

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

export const revalidate = 3600; // ISR: regenera cada hora

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 = '';

  // Mapa del sitio de páginas estáticas
  sitemaps += `
    <sitemap>
      <loc>${SITE_URL}/sitemap-pages.xml</loc>
      <lastmod>${now}</lastmod>
    </sitemap>`;

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

  // Mapas del sitio de celebridades
  for (let i = 0; i < celebrityChunks; i++) {
    sitemaps += `
    <sitemap>
      <loc>${SITE_URL}/sitemap-celebrities-${i}.xml</loc>
      <lastmod>${now}</lastmod>
    </sitemap>`;
  }

  // Mapas del sitio de localización
  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',
    },
  });
}

Nota que solo estamos haciendo consultas de count aquí -- head: true significa que Supabase devuelve solo el recuento sin datos de fila. Esto hace que la generación del índice del mapa del sitio sea casi instantánea.

Construir Mapas del Sitio Chunked Individuales

Aquí está el manejador del mapa del sitio de celebridades con paginación completa:

// 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('Índice de chunk inválido', { 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',
    },
  });
}

El mapa del sitio de blog sigue el mismo patrón pero con diferente prioridad y cambio de frecuencia:

// 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',
    },
  });
}

Necesitarás configurar tu enrutamiento de Next.js para manejar el segmento dinámico. En App Router, el nombre de la carpeta usa corchetes:

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 el enfoque de corchete en nombre de carpeta te causa problemas con tu sistema de archivos o IDE (a veces sucede), usa en su lugar reescrituras de ruta en 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;

Mapa del Sitio de Páginas Estáticas

Para el mapa del sitio de páginas estáticas, codificamos las URLs ya que rara vez cambian:

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

export const revalidate = 86400; // Una vez por día está bien para páginas estáticas

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',
    },
  });
}

Mapas del Sitio Localizados con Hreflang

Aquí es donde se pone interesante. Para contenido multilingüe, necesitas elementos xhtml:link con atributos hreflang. Cada mapa del sitio localizado referencia todas las versiones de idioma alternativo de cada página:

// 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('Localización inválida', { status: 404 });
  }

  const entries = await fetchAllSlugs('localized_pages');
  // Filtra páginas que tienen esta localización
  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',
    },
  });
}

Estrategia de Revalidación ISR

Establecemos revalidate = 3600 en todas las rutas del mapa del sitio. Eso significa que Vercel sirve el XML cacheado durante hasta una hora, luego lo regenera en segundo plano en la siguiente solicitud. Para 91K páginas, este es el punto dulce -- lo suficientemente frecuente para que el contenido nuevo se muestre el mismo día, pero no tan agresivo que estemos golpeando a Supabase constantemente.

Para revalidación bajo demanda cuando se publica contenido, agrega un punto final de revalidación:

// 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: 'No autorizado' }, { status: 401 });
  }

  // Revalida rutas de mapa del sitio específicas
  const targetPaths = paths || ['/sitemap.xml'];
  for (const path of targetPaths) {
    revalidatePath(path);
  }

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

Luego configura un Webhook de Base de Datos de Supabase (o un disparador de Postgres mediante pg_net) para llamar a este punto final siempre que se actualicen tus tablas celebrities o blog_posts.

Prioridad y Frecuencia de Cambio por Tipo de Contenido

Aquí está la matriz de prioridad que usamos. Google ha dicho que principalmente ignora priority y changefreq, pero otros rastreadores (Bing, Yandex) aún los usan, y no duelen:

Tipo de Contenido Prioridad Frecuencia de Cambio Justificación
Página de inicio 1.0 diaria Importancia máxima, se actualiza frecuentemente
Soluciones/Características 0.9 semanal Páginas de producto principal
Listado de blog 0.8 diaria Nuevas publicaciones regularmente
Publicaciones de blog 0.8 semanal Contenido actualizado ocasionalmente
Páginas de celebridades 0.6 mensual Raramente cambia después de la creación
Páginas localizadas 0.6 mensual Las actualizaciones de traducción son infrecuentes
Contacto/Legal 0.5 anual Casi nunca cambia

El valor lastmod es crítico y siempre debe provenir de la columna updated_at de tu base de datos -- nunca lo codifiques en duro a new Date(). Google usa lastmod para priorizar el rastreo, y si cada página dice que fue modificada en este momento, Google eventualmente ignorará tu lastmod.

Envío a Google Search Console

Aquí está la parte sencilla. En GSC:

  1. Ve a Sitemaps en la barra lateral izquierda
  2. Ingresa https://tudominio.com/sitemap.xml (solo la URL del índice)
  3. Haz clic en Enviar

Eso es todo. No envíes sub-mapas individuales. Google lee el índice y descubre automáticamente todos los hijos. Deberías ver el estado "Éxito" en pocas horas, y los recuentos de URLs indexadas subirán durante las próximas 2-4 semanas.

Para 91K URLs, espera que Google indexe el 70-90% dentro del primer mes. Las páginas restantes típicamente tienen contenido delgado, problemas de contenido duplicado, o simplemente son de baja prioridad en la asignación del presupuesto de rastreo de Google.

También agrega tu mapa del sitio a robots.txt:

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

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

Depuración Cuando Google No Indexará Tus Páginas

Aquí es donde la mayoría de la gente se atasca. Has enviado 91K URLs pero GSC muestra solo 40K indexadas. Aquí está la lista de verificación de depuración sistemática que seguimos:

Verifica si hay Etiquetas Noindex Accidentales

Esta es la causa #1. Haz una verificación puntual:

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

También verifica tu disposición de Next.js o metadatos de página. Un error común es configurar noindex en una disposición que se aplica a miles de páginas:

// MALO: Esto desindexa todas las páginas que usan esta disposición
export const metadata = {
  robots: { index: false, follow: true },
};

Verifica que robots.txt No Esté Bloqueando el Rastreo

Verifica https://tudominio.com/robots.txt en un navegador. Asegúrate de no estar bloqueando accidentalmente tus rutas dinámicas. En Vercel, también verifica si hay algo en el middleware que pueda estar devolviendo 403s a Googlebot.

Inspecciona Errores de Rastreo en GSC

Ve a PagesPor qué las páginas no están indexadas. Problemas comunes:

  • "Rastreada - actualmente no indexada": Google vio la página pero decidió no indexarla. Usualmente contenido delgado.
  • "Descubierta - actualmente no indexada": Google conoce la URL pero aún no la ha rastreado. Problema de presupuesto de rastreo.
  • "Excluida por etiqueta noindex": Por supuesto. Arregla la etiqueta.
  • "Duplicada sin canónica": Agrega etiquetas canónicas adecuadas.

Arregla Páginas Huérfanas con Enlaces Internos

Esto es enorme para sitios grandes. Si tus páginas de celebridades son solo descubribles a través del mapa del sitio y no tienen cero enlaces internos apuntando a ellas, Google despriorizará su rastreo. Agrega:

  • Páginas de categoría/listado que enlazan grupos de páginas de celebridades
  • Enlaces de celebridades relacionadas en cada página de celebridad
  • Secciones "Tendencias" o "Actualizado Recientemente" en páginas con mucho tráfico
  • Navegación de migas de pan con datos estructurados

Valida URLs Individuales

Usa la herramienta de Inspección de URL de GSC en páginas específicas que no están indexadas. Te muestra exactamente lo que Google ve -- el HTML renderizado, cualquier error, problemas de usabilidad móvil, y el estado de indexación.

Verifica Encabezados de Respuesta de Mapa del Sitio

Asegúrate de que tus rutas de mapa del sitio devuelvan encabezados adecuados:

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

Deberías ver Content-Type: application/xml y un estado 200. Si estás obteniendo respuestas 304 Not Modified de cachés obsoletos, eso puede hacer que Google se salte la relectura de tu mapa del sitio.

Rendimiento y Puntos de Referencia de Costos

Aquí están números reales de nuestro despliegue de producción a partir de principios de 2025:

Métrica Valor
URLs totales en mapa del sitio 91,247
Tiempo de generación del índice de mapa del sitio ~120ms (solo consultas de recuento)
Generación de mapa del sitio individual (50K URLs) ~4.2 segundos
Costo de consulta de Supabase por regeneración de mapa del sitio ~$0.01
Tamaño total de XML del mapa del sitio (todos los archivos combinados) ~8.4MB sin comprimir
Ancho de banda de Vercel para mapas del sitio por mes ~2.1GB (principalmente Googlebot)
Costo del plan Pro de Vercel $20/usuario/mes
Costo del plan Pro de Supabase $25/mes
Tasa de indexación de GSC después de 30 días 84% de URLs enviadas
Tiempo desde la publicación de contenido hasta la actualización del mapa del sitio ≤1 hora (ISR) o ~5 segundos (bajo demanda)

La gran conclusión: todo este setup cuesta básicamente nada para ejecutar. La generación del mapa del sitio es un error de redondeo en tus facturas de Vercel y Supabase.

Si estás construyendo un proyecto similar a gran escala y quieres ayuda con la arquitectura, lo hemos hecho en múltiples sitios de clientes. Consulta nuestras capacidades de desarrollo de Next.js o nuestro trabajo de desarrollo de CMS headless. Para sitios basados en Astro con requisitos de escala similares, hemos construido soluciones comparables usando el enfoque de punto final de Astro.

El código de trabajo completo está disponible como un gist de GitHub: todos los manejadores de ruta, la biblioteca de consultas de Supabase, y las reescrituras en next.config.ts. Si tu proyecto necesita algo más personalizado -- mapas del sitio multi-inquilino, revalidación en tiempo real, o mapas del sitio para 1M+ páginas -- comunícate con nosotros y lo delimitaremos.

Preguntas Frecuentes

¿Cuántas URLs puede contener un único archivo de mapa del sitio? El protocolo de mapa del sitio permite un máximo de 50,000 URLs por archivo y 50MB de archivo sin comprimir. Para sitios con más de 50K páginas, necesitas un índice de mapa del sitio que referencia múltiples archivos de mapa del sitio chunked. En la práctica, la mayoría de los generadores de mapas del sitio chuenkean en 45,000-50,000 URLs para dejar un margen de seguridad.

¿Debería usar next-sitemap o construir manejadores de ruta personalizados? next-sitemap (v4+) es excelente para configuraciones más simples y maneja el auto-chunkeo bien. Pero para 91K+ páginas dinámicas con prioridades específicas por tipo de contenido, mapas del sitio localizados con hreflang, y control granular de ISR, los manejadores de ruta personalizados te dan más control. Fuimos personalizados porque necesitábamos intervalos de revalidación diferentes por tipo de contenido y queríamos que la estructura del mapa del sitio coincidiera con nuestro flujo de trabajo de depuración de GSC.

¿Envío cada archivo individual de mapa del sitio a Google Search Console? No. Envía solo la URL del índice del mapa del sitio (ej., https://tudominio.com/sitemap.xml). Google lee el índice y descubre y procesa automáticamente todos los sub-mapas referenciados. Enviar archivos individuales es innecesario y desordena tu panel de GSC.

¿Con qué frecuencia deben regenerarse los mapas del sitio para sitios dinámicos grandes? Para la mayoría de los sitios con mucho contenido, la regeneración cada hora mediante ISR (revalidate = 3600) es un buen predeterminado. Si publicas contenido muy frecuentemente, emparéjalo con revalidación bajo demanda activada por webhooks de base de datos. No regeneres en cada solicitud -- eso anula el cacheo y aumenta innecesariamente la carga de Supabase.

¿Por qué Google no está indexando todas mis URLs del mapa del sitio? Las causas más comunes son: etiquetas meta noindex accidentales, robots.txt bloqueando, contenido delgado/duplicado, páginas huérfanas sin enlaces internos, y limitaciones del presupuesto de rastreo. Verifica el reporte de "Páginas" de GSC bajo "Por qué las páginas no están indexadas" para razones específicas. Para sitios grandes, enfócate en mejorar los enlaces internos a páginas huérfanas -- esto es a menudo la palanca individual más grande.

¿El valor priority en los mapas del sitio realmente afecta las clasificaciones de Google? Google ha declarado públicamente que en gran medida ignoran los valores priority y changefreq. Sin embargo, Bing y otros motores de búsqueda sí los usan. El campo lastmod es la señal del mapa del sitio más importante -- asegúrate de que refleje cambios de contenido reales de tu base de datos, no la marca de tiempo actual.

¿Cómo manejo el límite de 1,000 filas de Supabase para consultas del mapa del sitio? Usa el método .range(offset, offset + batchSize - 1) de Supabase para paginar en lotes de 1,000. Haz un bucle hasta que hayas obtenido todas las filas para el chunk del mapa del sitio actual. Para consultas de solo recuento (usadas en el índice del mapa del sitio), usa .select('*', { count: 'exact', head: true }) que devuelve solo el recuento sin transferir datos de fila.

¿Puede este enfoque manejar 500K o 1 millón de páginas? Sí, con ajustes menores. La arquitectura chunked se escala linealmente -- 1 millón de páginas produciría aproximadamente 20 sub-mapas. La preocupación principal se convierte en el tiempo de espera de función serverless de 60 segundos de Vercel para generar mapas del sitio individuales de 50K URLs. Si alcanzas ese límite, reduce el tamaño del chunk a 25,000 o 10,000 URLs por archivo. El protocolo de mapa del sitio permite hasta 50,000 mapas del sitio en un único índice, así que no te encontrarás con límites a nivel de índice en el futuro previsible.