Construindo um Sitemap Dinâmico para 91 mil Páginas com Next.js e Supabase
Construindo um Sitemap Dinâmico para 91.000 Páginas com Next.js e Supabase
Mês passado atingimos 91.000 páginas no Deluxe Astrology. Gráficos de nascimento de celebridades, posts de blog, conteúdo localizado em seis idiomas -- o site havia crescido muito além do que um único arquivo sitemap poderia lidar. O protocolo de sitemap do Google limita você a 50.000 URLs por arquivo e 50MB descompactado. Precisávamos de um índice de sitemap com sub-sitemaps divididos em chunks, todos gerados dinamicamente a partir do Supabase, em cache com ISR no Vercel, e enviados ao Google Search Console como uma única URL de índice.
Esta é a implementação exata que lançamos. Não é uma explicação teórica -- código real de produção que lida com 91K URLs hoje e dimensionará para 500K sem mudanças.
Índice
- Entendendo Limites de Sitemap e Arquitetura
- A Estrutura de Sitemap do Deluxe Astrology
- Configurando Consultas do Supabase com Paginação de Deslocamento
- Construindo a Rota de Índice de Sitemap
- Construindo Sitemaps Individuais com Chunks
- Sitemap de Páginas Estáticas
- Sitemaps Localizados com Hreflang
- Estratégia de Revalidação ISR
- Prioridade e Frequência de Mudança por Tipo de Conteúdo
- Envio do Google Search Console
- Depurando Quando o Google Não Indexará Suas Páginas
- Benchmarks de Desempenho e Custo
- Perguntas Frequentes

Entendendo Limites de Sitemap e Arquitetura
Aqui estão os limites rígidos que você precisa saber:
| Restrição | Limite | Fonte |
|---|---|---|
| URLs por arquivo de sitemap | 50.000 | protocolo sitemaps.org |
| Tamanho do arquivo por sitemap | 50MB descompactado | protocolo sitemaps.org |
| Sitemaps por índice de sitemap | 50.000 | protocolo sitemaps.org |
Máximo de .range() do Supabase por consulta |
1.000 linhas (padrão) | Configuração PostgREST do Supabase |
| Limite de tempo da função serverless do Vercel (Pro) | 60 segundos | docs Vercel 2025 |
| Limite de tamanho do corpo da resposta do Vercel | 10MB | cache edge do Vercel |
Para 91.000 URLs, você precisa de no mínimo dois arquivos de sitemap. Mas não apenas despejamos tudo em dois buckets de 50K URLs. Dividimos por tipo de conteúdo -- celebridades, posts de blog, páginas estáticas, páginas localizadas -- porque cada tipo tem diferentes changefreq, priority e padrões de atualização. Isso nos dá melhor controle e facilita muito a depuração no GSC quando algo dá errado.
A Estrutura de Sitemap do Deluxe Astrology
Aqui está o que a arquitetura final do sitemap parece:
/sitemap.xml → Índice de Sitemap (aponta para todos os sub-sitemaps)
/sitemap-pages.xml → Páginas estáticas (~30 URLs)
/sitemap-blog-0.xml → Posts de blog chunk 0 (até 50K)
/sitemap-blog-1.xml → Posts de blog chunk 1 (overflow)
/sitemap-celebrities-0.xml → Páginas de celebridades chunk 0 (até 50K)
/sitemap-celebrities-1.xml → Páginas de celebridades chunk 1 (overflow)
/sitemap-locale-es.xml → Páginas localizadas em espanhol
/sitemap-locale-fr.xml → Páginas localizadas em francês
/sitemap-locale-de.xml → Páginas localizadas em alemão
/sitemap-locale-pt.xml → Páginas localizadas em português
/sitemap-locale-ja.xml → Páginas localizadas em japonês
Cada sub-sitemap é um manipulador de rota do App Router do Next.js que consulta o Supabase em tempo real, gera XML e faz cache via ISR com revalidate = 3600 (por hora). O índice de sitemap em si também é um manipulador de rota.
Configurando Consultas do Supabase com Paginação de Deslocamento
Aqui está a peça crítica que a maioria dos tutoriais acerta errado: você não pode apenas fazer supabase.from('celebrities').select('*') e esperar 91.000 linhas. A camada PostgREST do Supabase padrão é retornar no máximo 1.000 linhas. Você precisa paginar.
Usamos paginação de deslocamento baseada em intervalo em 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! // Use service role para lado 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(`Erro de busca de sitemap 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 }> {
// Primeiro obtenha a contagem total para o índice de 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);
}
Algumas coisas a observar aqui. Usamos a SUPABASE_SERVICE_ROLE_KEY -- não a chave anon -- porque esses manipuladores de rota são executados no lado do servidor e não queremos que políticas RLS desacelerem nossas consultas de sitemap. A função fetchSlugsChunked apenas busca o chunk específico necessário para um arquivo de sitemap determinado, não todo o conjunto de dados. Isso importa quando você está executando no limite de timeout de 60 segundos da função serverless do Vercel.

Construindo a Rota de Índice de Sitemap
O índice de sitemap é a única URL que você envia para o Google. Ele faz referência a todos os seus sub-sitemaps.
// app/sitemap.xml/route.ts
import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
export const revalidate = 3600; // ISR: regenerar a 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 = '';
// Sitemap de páginas estáticas
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 celebridades
for (let i = 0; i < celebrityChunks; i++) {
sitemaps += `
<sitemap>
<loc>${SITE_URL}/sitemap-celebrities-${i}.xml</loc>
<lastmod>${now}</lastmod>
</sitemap>`;
}
// Sitemaps de locale
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',
},
});
}
Observe que estamos apenas fazendo consultas count aqui -- head: true significa que Supabase retorna apenas a contagem sem dados de linha. Isso torna a geração do índice de sitemap quase instantânea.
Construindo Sitemaps Individuais com Chunks
Aqui está o manipulador de sitemap de celebridades com paginação 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',
},
});
}
O sitemap de blog segue o mesmo padrão, mas com prioridade e changefreq diferentes:
// 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',
},
});
}
Você precisará configurar o roteamento do Next.js para lidar com o segmento dinâmico. No App Router, o nome da pasta usa colchetes:
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
Se a abordagem de colchetes no nome da pasta causar problemas com seu sistema de arquivos ou IDE (às vezes causa), use reescritas de rota em 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 de Páginas Estáticas
Para o sitemap de páginas estáticas, codificamos as URLs porque raramente mudam:
// app/sitemap-pages.xml/route.ts
import { NextResponse } from 'next/server';
export const revalidate = 86400; // Uma vez por dia está bem 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',
},
});
}
Sitemaps Localizados com Hreflang
É aqui que fica interessante. Para conteúdo multilíngue, você precisa de elementos xhtml:link com atributos hreflang. Cada sitemap localizado faz referência a todas as versões 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('Locale inválido', { status: 404 });
}
const entries = await fetchAllSlugs('localized_pages');
// Filtrar para páginas que têm este 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',
},
});
}
Estratégia de Revalidação ISR
Definimos revalidate = 3600 em todas as rotas de sitemap. Isso significa que o Vercel serve o XML em cache por até uma hora e, em seguida, o regenera em segundo plano na próxima solicitação. Para 91K páginas, este é o ponto doce -- frequente o suficiente para que conteúdo novo apareça no mesmo dia, mas não tão agressivo que estamos martelando o Supabase.
Para revalidação sob demanda quando o conteúdo é publicado, adicione um endpoint de revalidação:
// 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: 'Não autorizado' }, { status: 401 });
}
// Revalidar caminhos específicos de sitemap
const targetPaths = paths || ['/sitemap.xml'];
for (const path of targetPaths) {
revalidatePath(path);
}
return NextResponse.json({ revalidated: true, paths: targetPaths });
}
Em seguida, configure um Database Webhook do Supabase (ou um gatilho Postgres via pg_net) para chamar este endpoint sempre que suas tabelas celebrities ou blog_posts forem atualizadas.
Prioridade e Frequência de Mudança por Tipo de Conteúdo
Aqui está a matriz de prioridade que usamos. Google disse que na maioria das vezes ignora priority e changefreq, mas outros rastreadores (Bing, Yandex) ainda os usam, e eles não prejudicam:
| Tipo de Conteúdo | Prioridade | Frequência de Mudança | Justificativa |
|---|---|---|---|
| Homepage | 1.0 | daily | Importância máxima, atualizado frequentemente |
| Soluções/Recursos | 0.9 | weekly | Páginas de produtos principais |
| Listagem de blog | 0.8 | daily | Novos posts regularmente |
| Posts de blog | 0.8 | weekly | Conteúdo atualizado ocasionalmente |
| Páginas de celebridades | 0.6 | monthly | Raramente muda após criação |
| Páginas localizadas | 0.6 | monthly | Atualizações de tradução são infrequentes |
| Contato/Legal | 0.5 | yearly | Quase nunca muda |
O valor lastmod é crítico e deve sempre vir da coluna updated_at do seu banco de dados -- nunca codifique isso para new Date(). Google usa lastmod para priorizar recrawl, e se cada página disser que foi modificada agora, Google eventualmente ignorará completamente seu lastmod.
Envio do Google Search Console
Aqui está a parte direta. No GSC:
- Vá para Sitemaps na barra lateral esquerda
- Digite
https://seudominio.com/sitemap.xml(apenas a URL do índice) - Clique em Enviar
É isso. Não envie sub-sitemaps individuais. Google lê o índice e descobre automaticamente todos os filhos. Você deve ver o status "Sucesso" dentro de algumas horas, e as contagens de URLs indexadas subirão nos próximos 2-4 semanas.
Para 91K URLs, espere que o Google indexe 70-90% no primeiro mês. As páginas restantes tipicamente têm conteúdo fino, problemas de conteúdo duplicado, ou são simplesmente baixa prioridade na alocação do orçamento de crawl do Google.
Também adicione seu sitemap a robots.txt:
# robots.txt
User-agent: *
Allow: /
Sitemap: https://deluxeastrology.com/sitemap.xml
Depurando Quando o Google Não Indexará Suas Páginas
É aqui que a maioria das pessoas fica presa. Você enviou 91K URLs, mas o GSC mostra apenas 40K indexados. Aqui está a lista de verificação de depuração sistemática que seguimos:
Verificar Tags Noindex Acidentais
Esta é a causa #1. Execute uma verificação pontual:
curl -s https://deluxeastrology.com/celebrities/some-slug | grep -i 'noindex'
Também verifique seus metadados de layout ou página do Next.js. Um erro comum é definir noindex em um layout que se aplica a milhares de páginas:
// RUIM: Isto noindexes todas as páginas usando este layout
export const metadata = {
robots: { index: false, follow: true },
};
Verificar se robots.txt Não Está Bloqueando o Crawling
Verifique https://seudominio.com/robots.txt em um navegador. Certifique-se de que você não está bloqueando acidentalmente suas rotas dinâmicas. No Vercel, também verifique se há middleware que possa estar retornando 403s ao Googlebot.
Inspecionar Erros de Crawl no GSC
Vá para Páginas → Por que as páginas não estão indexadas. Problemas comuns:
- "Rastreado - atualmente não indexado": Google viu a página, mas decidiu não indexá-la. Geralmente conteúdo fino.
- "Descoberto - atualmente não indexado": Google sabe que a URL existe, mas ainda não a rastreou. Problema de orçamento de crawl.
- "Excluído por tag noindex": Autoexplicativo. Corrija a tag.
- "Duplicado sem canonical": Adicione tags canônicas adequadas.
Corrija Páginas Órfãs com Link Interno
Isto é enorme para sites grandes. Se suas páginas de celebridades são apenas descobríveis através do sitemap e têm zero links internos apontando para elas, o Google depriorizará o rastreamento delas. Adicione:
- Páginas de categoria/listagem que vinculam a grupos de páginas de celebridades
- Links de celebridades relacionadas em cada página de celebridade
- Seções "Em Destaque" ou "Atualizado Recentemente" em páginas de alto tráfego
- Navegação breadcrumb com dados estruturados
Validar URLs Individuais
Use a ferramenta de Inspeção de URL do GSC em páginas específicas que não estão indexadas. Mostra exatamente o que o Google vê -- o HTML renderizado, qualquer erro, problemas de usabilidade móvel e o status de indexação.
Verificar Headers de Resposta de Sitemap
Certifique-se de que suas rotas de sitemap retornam headers apropriados:
curl -I https://deluxeastrology.com/sitemap.xml
Você deve ver Content-Type: application/xml e um status 200. Se você estiver recebendo respostas 304 Not Modified de caches obsoletos, isso pode fazer com que o Google pule a releitura de seu sitemap.
Benchmarks de Desempenho e Custo
Aqui estão números reais de nossa implantação de produção em janeiro de 2025:
| Métrica | Valor |
|---|---|
| Total de URLs no sitemap | 91.247 |
| Tempo de geração do índice de sitemap | ~120ms (apenas consultas de contagem) |
| Geração individual de sitemap (50K URLs) | ~4,2 segundos |
| Custo de consulta do Supabase por regeneração de sitemap | ~$0,01 |
| Tamanho total de XML de sitemap (todos os arquivos combinados) | ~8,4MB descompactado |
| Largura de banda do Vercel para sitemaps por mês | ~2,1GB (principalmente Googlebot) |
| Custo do plano Vercel Pro | $20/usuário/mês |
| Custo do plano Supabase Pro | $25/mês |
| Taxa de indexação do GSC após 30 dias | 84% das URLs enviadas |
| Tempo de publicação de conteúdo para atualização de sitemap | ≤1 hora (ISR) ou ~5 segundos (sob demanda) |
O grande aprendizado: toda essa configuração custa basicamente nada para ser executada. A geração do sitemap é um arredondamento na sua fatura do Vercel e Supabase.
Se você está construindo um projeto de grande escala similar e quer ajuda com a arquitetura, fizemos isso em vários sites de clientes. Confira nossas capacidades de desenvolvimento Next.js ou nosso trabalho de desenvolvimento de CMS headless. Para sites baseados em Astro com requisitos de escala similar, construímos soluções comparáveis usando abordagem de endpoint do Astro.
O código completo funcionando está disponível como um gist do GitHub: todos os manipuladores de rota, a biblioteca de consulta Supabase e as reescritas next.config.ts. Se seu projeto precisar de algo mais customizado -- sitemaps multi-tenant, revalidação em tempo real ou sitemaps para 1M+ páginas -- entre em contato conosco e faremos a avaliação.
Perguntas Frequentes
Quantas URLs um arquivo de sitemap único pode conter? O protocolo de sitemap permite um máximo de 50.000 URLs por arquivo e arquivo de tamanho descompactado de 50MB. Para sites com mais de 50K páginas, você precisa de um índice de sitemap que referencia múltiplos arquivos de sitemap divididos em chunks. Na prática, a maioria dos geradores de sitemap fazem chunk em 45.000-50.000 URLs para deixar uma margem de segurança.
Devo usar next-sitemap ou construir manipuladores de rota customizados? next-sitemap (v4+) é ótimo para configurações mais simples e lida bem com auto-chunking. Mas para 91K+ páginas dinâmicas com prioridades específicas por tipo de conteúdo, sitemaps localizados com hreflang e controle de ISR fino, manipuladores de rota customizados te dão mais controle. Fomos com customizado porque precisávamos de intervalos de revalidação diferentes por tipo de conteúdo e queríamos que a estrutura do sitemap correspondesse ao nosso fluxo de trabalho de depuração do GSC.
Devo enviar cada arquivo individual de sitemap para o Google Search Console?
Não. Envie apenas a URL do índice de sitemap (ex: https://seudominio.com/sitemap.xml). Google lê o índice e descobre e processa automaticamente todos os sub-sitemaps referenciados. Enviar arquivos individuais é desnecessário e desorganiza seu painel do GSC.
Com que frequência sitemaps devem ser regenerados para sites dinâmicos grandes?
Para a maioria dos sites ricos em conteúdo, regeneração por hora via ISR (revalidate = 3600) é um bom padrão. Se você publica conteúdo muito frequentemente, emparelhe com revalidação sob demanda acionada por webhooks de banco de dados. Não regenere em cada solicitação -- isso derrota o cache e aumenta desnecessariamente a carga do Supabase.
Por que o Google não está indexando todas as minhas URLs de sitemap? As causas mais comuns são: tags meta noindex acidentais, robots.txt bloqueando, conteúdo fino/duplicado, páginas órfãs sem links internos e limitações do orçamento de crawl. Verifique o relatório "Páginas" do GSC sob "Por que as páginas não estão indexadas" para razões específicas. Para sites grandes, concentre-se em melhorar a link interno para páginas órfãs -- este é frequentemente o maior alavanca única.
O valor de priority no sitemap realmente afeta as classificações do Google?
Google declarou publicamente que largamente ignora valores de priority e changefreq. Porém, Bing e outros mecanismos de busca os usam. O campo lastmod é o sinal de sitemap mais importante -- certifique-se de que reflete mudanças reais de conteúdo do seu banco de dados, não o timestamp atual.
Como lido com o limite de 1.000 linhas do Supabase para consultas de sitemap?
Use o método .range(offset, offset + batchSize - 1) do Supabase para paginar em lotes de 1.000. Faça loop até ter buscado todas as linhas para o chunk de sitemap atual. Para consultas apenas de contagem (usadas no índice de sitemap), use .select('*', { count: 'exact', head: true }) que retorna apenas a contagem sem transferir nenhum dado de linha.
Esta abordagem pode lidar com 500K ou 1 milhão de páginas? Sim, com ajustes menores. A arquitetura em chunks dimensiona linearmente -- 1 milhão de páginas produziria cerca de 20 sub-sitemaps. A principal preocupação se torna o limite de timeout de 60 segundos do Vercel para gerar sitemaps individuais de 50K URLs. Se você atingir esse limite, reduza o tamanho do chunk para 25.000 ou 10.000 URLs por arquivo. O protocolo de sitemap permite até 50.000 sitemaps em um índice único, então você não vai encontrar limites de nível de índice em breve.