Übersetzung folgt:


Letzten Monat haben wir 91.000 Seiten auf Deluxe Astrology erreicht. Prominenten-Geburtshoroskope, Blog-Posts, lokalisierte Inhalte in sechs Sprachen -- die Website war weit über das hinausgewachsen, was eine einzelne Sitemap-Datei bewältigen konnte. Googles Sitemap-Protokoll begrenzt Sie auf 50.000 URLs pro Datei und 50 MB unkomprimiert. Wir brauchten einen Sitemap-Index mit chunked Sub-Sitemaps, alle dynamisch aus Supabase generiert, mit ISR auf Vercel gecacht und als einzelne Index-URL an die Google Search Console übermittelt.

Dies ist die exakte Implementierung, die wir in Betrieb genommen haben. Keine theoretische Anleitung -- echter Produktionscode, der heute 91.000 URLs verwaltet und sich auf 500.000 skalieren lässt, ohne dass Änderungen erforderlich sind.

Inhaltsverzeichnis

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

Sitemap-Limits und Architektur verstehen

Hier sind die harten Limits, die Sie kennen müssen:

Einschränkung Limit Quelle
URLs pro Sitemap-Datei 50.000 sitemaps.org-Protokoll
Dateigröße pro Sitemap 50 MB unkomprimiert sitemaps.org-Protokoll
Sitemaps pro Sitemap-Index 50.000 sitemaps.org-Protokoll
Supabase .range() Max pro Abfrage 1.000 Zeilen (Standard) Supabase PostgREST-Konfiguration
Vercel Serverless-Funktions-Timeout (Pro) 60 Sekunden Vercel-Dokumentation 2025
Vercel Response-Body-Größenlimit 10 MB Vercel Edge-Caching

Für 91.000 URLs benötigen Sie mindestens zwei Sitemap-Dateien. Aber wir schmeißen nicht einfach alles in zwei 50.000-URL-Buckets. Wir teilen nach Content-Typ auf -- Prominente, Blog-Posts, statische Seiten, lokalisierte Seiten -- denn jeder Typ hat unterschiedliche changefreq, priority und Update-Muster. Das gibt uns bessere Kontrolle und macht das Debugging in GSC viel einfacher, wenn etwas schiefgeht.

Die Sitemap-Struktur für Deluxe Astrology

Hier sieht die endgültige Sitemap-Architektur aus:

/sitemap.xml                    → Sitemap-Index (verweist auf alle Sub-Sitemaps)
  /sitemap-pages.xml            → Statische Seiten (~30 URLs)
  /sitemap-blog-0.xml           → Blog-Posts Chunk 0 (bis zu 50K)
  /sitemap-blog-1.xml           → Blog-Posts Chunk 1 (Overflow)
  /sitemap-celebrities-0.xml    → Prominenten-Seiten Chunk 0 (bis zu 50K)
  /sitemap-celebrities-1.xml    → Prominenten-Seiten Chunk 1 (Overflow)
  /sitemap-locale-es.xml        → Spanische lokalisierte Seiten
  /sitemap-locale-fr.xml        → Französische lokalisierte Seiten
  /sitemap-locale-de.xml        → Deutsche lokalisierte Seiten
  /sitemap-locale-pt.xml        → Portugiesische lokalisierte Seiten
  /sitemap-locale-ja.xml        → Japanische lokalisierte Seiten

Jede Sub-Sitemap ist ein Next.js App Router Route Handler, der Supabase zur Laufzeit abfragt, XML generiert und über ISR mit revalidate = 3600 (stündlich) cacht. Der Sitemap-Index selbst ist auch ein Route Handler.

Supabase-Abfragen mit Offset-Paginierung einrichten

Hier ist das kritische Stück, das die meisten Tutorials falsch machen: Sie können nicht einfach supabase.from('celebrities').select('*') machen und erwarten, 91.000 Zeilen zurückzubekommen. Supabase's PostgREST-Schicht gibt standardmäßig maximal 1.000 Zeilen zurück. Sie müssen paginieren.

Wir verwenden bereichsbasierte Offset-Paginierung in Batches von 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! // Verwenden Sie Service Role für Server-seitig
);

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(`Sitemap fetch error for ${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 }> {
  // Rufen Sie zunächst die Gesamtanzahl für den Sitemap-Index ab
  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);
}

Es gibt hier ein paar Dinge zu beachten. Wir verwenden den SUPABASE_SERVICE_ROLE_KEY -- nicht den anon Key -- denn diese Route Handler laufen server-seitig und wir wollen nicht, dass RLS-Richtlinien unsere Sitemap-Abfragen verlangsamen. Die fetchSlugsChunked-Funktion ruft nur den spezifischen Chunk ab, der für eine bestimmte Sitemap-Datei benötigt wird, nicht den gesamten Datensatz. Das ist wichtig, wenn Sie bei Vercel's 60-Sekunden-Funktions-Timeout arbeiten.

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

Den Sitemap-Index-Route erstellen

Der Sitemap-Index ist die einzelne URL, die Sie an Google übermitteln. Er referenziert alle Ihre Sub-Sitemaps.

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

export const revalidate = 3600; // ISR: alle Stunde neu generieren

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 für statische Seiten
  sitemaps += `
    <sitemap>
      <loc>${SITE_URL}/sitemap-pages.xml</loc>
      <lastmod>${now}</lastmod>
    </sitemap>`;

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

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

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

Beachten Sie, dass wir hier nur count-Abfragen durchführen -- head: true bedeutet, dass Supabase nur die Anzahl zurückgibt, ohne irgendwelche Zeilendaten. Dies macht die Sitemap-Index-Generierung nahezu augenblicklich.

Individuelle Chunked-Sitemaps erstellen

Hier ist der Prominenten-Sitemap-Handler mit vollständiger Paginierung:

// 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('Invalid chunk index', { 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',
    },
  });
}

Die Blog-Sitemap folgt dem gleichen Muster, aber mit unterschiedlicher Priorität und Änderungshäufigkeit:

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

Sie müssen Ihr Next.js Routing so konfigurieren, dass es das dynamische Segment verwaltet. Im App Router verwendet der Ordnername Klammern:

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

Falls der Klammer-im-Ordnernamen-Ansatz Ihnen Probleme mit Ihrem Dateisystem oder IDE bereitet (das kommt manchmal vor), verwenden Sie stattdessen Route Rewrites in 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 für statische Seiten

Für die Sitemap für statische Seiten hardcoden wir die URLs, da sie sich selten ändern:

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

export const revalidate = 86400; // Einmal pro Tag reicht für statische Seiten

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

Lokalisierte Sitemaps mit Hreflang

Hier wird es interessant. Für mehrsprachige Inhalte benötigen Sie xhtml:link-Elemente mit hreflang-Attributen. Jede lokalisierte Sitemap referenziert alle alternativen Sprachversionen jeder Seite:

// 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('Invalid locale', { status: 404 });
  }

  const entries = await fetchAllSlugs('localized_pages');
  // Filtern Sie auf Seiten, die diese Locale haben
  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',
    },
  });
}

ISR-Revalidierungsstrategie

Wir setzen revalidate = 3600 auf allen Sitemap-Routes. Das bedeutet, dass Vercel das gecachte XML bis zu einer Stunde lang bereitstellt und es dann im Hintergrund bei der nächsten Anfrage neu generiert. Für 91.000 Seiten ist dies das ideale Verhältnis -- häufig genug, dass neuer Inhalt am selben Tag angezeigt wird, aber nicht aggressiv genug, dass wir Supabase mit Hammerschlägen bearbeiten.

Für On-Demand-Revalidierung, wenn Inhalte veröffentlicht werden, fügen Sie einen Revalidierungs-Endpunkt hinzu:

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

  // Validieren Sie spezifische Sitemap-Pfade neu
  const targetPaths = paths || ['/sitemap.xml'];
  for (const path of targetPaths) {
    revalidatePath(path);
  }

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

Richten Sie dann einen Supabase Database Webhook ein (oder einen Postgres-Trigger über pg_net), um diesen Endpunkt aufzurufen, wenn Ihre celebrities- oder blog_posts-Tabellen aktualisiert werden.

Priorität und Änderungshäufigkeit nach Content-Typ

Hier ist die Prioritätsmatrix, die wir verwenden. Google hat gesagt, dass es priority und changefreq größtenteils ignoriert, aber andere Crawler (Bing, Yandex) verwenden sie noch immer, und sie schaden nicht:

Content-Typ Priorität Änderungshäufigkeit Begründung
Homepage 1.0 täglich Höchste Wichtigkeit, häufig aktualisiert
Lösungen/Funktionen 0.9 wöchentlich Kern-Produktseiten
Blog-Auflistung 0.8 täglich Neue Posts regelmäßig
Blog-Posts 0.8 wöchentlich Inhalt gelegentlich aktualisiert
Prominenten-Seiten 0.6 monatlich Ändert sich nach Erstellung selten
Lokalisierte Seiten 0.6 monatlich Übersetzungsaktualisierungen sind selten
Kontakt/Rechtliche 0.5 jährlich Ändert sich fast nie

Der lastmod-Wert ist entscheidend und sollte immer aus der updated_at-Spalte Ihrer Datenbank stammen -- coden Sie ihn niemals auf new Date() ein. Google verwendet lastmod, um das Neucrawling zu priorisieren. Wenn jede Seite sagt, dass sie gerade eben geändert wurde, wird Google lastmod schließlich völlig ignorieren.

Übermittlung an Google Search Console

Dies ist der unkomplizierte Teil. In GSC:

  1. Gehen Sie zu Sitemaps in der linken Seitenleiste
  2. Geben Sie https://yourdomain.com/sitemap.xml ein (nur die Index-URL)
  3. Klicken Sie auf Senden

Das ist alles. Reichen Sie keine einzelnen Sub-Sitemaps ein. Google liest den Index und entdeckt alle untergeordneten Elemente automatisch. Der Status sollte innerhalb weniger Stunden "Erfolg" anzeigen, und die indizierten URL-Zählwerte steigen über die nächsten 2-4 Wochen.

Für 91.000 URLs können Sie erwarten, dass Google 70-90% im ersten Monat indexiert. Die verbleibenden Seiten haben normalerweise schwache Inhalte, Duplicate-Content-Probleme oder sind einfach niedrig priorisiert in Googles Crawl-Budget-Zuweisung.

Fügen Sie Ihre Sitemap auch zu robots.txt hinzu:

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

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

Debugging, wenn Google Ihre Seiten nicht indexiert

Hier bleiben die meisten Menschen stecken. Sie haben 91.000 URLs eingereicht, aber GSC zeigt nur 40.000 indexierte URLs. Hier ist die systematische Debugging-Checkliste, die wir befolgen:

Überprüfen Sie auf versehentliche Noindex-Tags

Dies ist die #1-Ursache. Führen Sie eine Stichprobenprüfung durch:

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

Überprüfen Sie auch Ihre Next.js Layout- oder Seiten-Metadaten. Ein häufiger Fehler ist das Setzen von noindex in einem Layout, das auf Tausende von Seiten angewendet wird:

// SCHLECHT: Dies indexiert alle Seiten, die dieses Layout verwenden, nicht
export const metadata = {
  robots: { index: false, follow: true },
};

Überprüfen Sie, dass robots.txt das Crawlen nicht blockiert

Überprüfen Sie https://yourdomain.com/robots.txt in einem Browser. Stellen Sie sicher, dass Sie Ihre dynamischen Routes nicht versehentlich blockieren. Überprüfen Sie auf Vercel auch Middleware, die möglicherweise 403s an Googlebot zurückgibt.

Untersuchen Sie Crawl-Fehler in GSC

Gehen Sie zu SeitenWarum Seiten nicht indexiert sind. Häufige Probleme:

  • "Gecrawlt - derzeit nicht indexiert": Google sah die Seite, entschied sich aber, sie nicht zu indexieren. Normalerweise schwache Inhalte.
  • "Entdeckt - derzeit nicht indexiert": Google weiß, dass die URL existiert, hat sie aber noch nicht gecrawlt. Crawl-Budget-Problem.
  • "Ausgeschlossen durch Noindex-Tag": Selbsterklärend. Beheben Sie das Tag.
  • "Duplikat ohne Canonical": Fügen Sie ordnungsgemäße Canonical-Tags hinzu.

Dies ist riesig für große Sites. Wenn Ihre Prominenten-Seiten nur über die Sitemap auffindbar sind und null interne Links auf sie verweisen, wird Google das Crawlen von ihnen deprioritieren. Fügen Sie hinzu:

  • Kategorie-/Auflistungsseiten, die auf Gruppen von Prominenten-Seiten verlinken
  • Zugehörige Prominenten-Links auf jeder Prominenten-Seite
  • "Trending" oder "Kürzlich aktualisiert"-Abschnitte auf Traffic-starken Seiten
  • Breadcrumb-Navigation mit strukturierten Daten

Überprüfen Sie einzelne URLs

Verwenden Sie GSC's URL-Inspektions-Tool auf bestimmten Seiten, die nicht indexiert sind. Es zeigt Ihnen genau, was Google sieht -- das gerenderte HTML, alle Fehler, Mobile-Usability-Probleme und den Indexierungsstatus.

Überprüfen Sie Sitemap-Response-Header

Stellen Sie sicher, dass Ihre Sitemap-Routes ordnungsgemäße Header zurückgeben:

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

Sie sollten Content-Type: application/xml und einen 200-Status sehen. Wenn Sie 304 Not Modified-Antworten von veralteten Caches erhalten, kann dies Google veranlassen, Ihre Sitemap erneut zu lesen.

Performance- und Kostenvergleiche

Hier sind echte Zahlen aus unserer Produktionsbereitstellung ab Anfang 2025:

Metrik Wert
Gesamte URLs in Sitemap 91.247
Sitemap-Index-Generierungszeit ~120 ms (nur Count-Abfragen)
Individuelle Sitemap-Generierung (50K URLs) ~4,2 Sekunden
Supabase-Abfragekosten pro Sitemap-Neugenerierung ~0,01 USD
Gesamte Sitemap-XML-Größe (alle Dateien kombiniert) ~8,4 MB unkomprimiert
Vercel-Bandbreite für Sitemaps pro Monat ~2,1 GB (größtenteils Googlebot)
Vercel Pro-Plan-Kosten 20 USD/Benutzer/Monat
Supabase Pro-Plan-Kosten 25 USD/Monat
GSC-Indexierungsrate nach 30 Tagen 84% der eingereichten URLs
Zeit von der Inhaltsveröffentlichung bis zur Sitemap-Aktualisierung ≤1 Stunde (ISR) oder ~5 Sekunden (On-Demand)

Die große Erkenntnis: Dieser gesamte Setup kostet praktisch nichts, um ihn zu betreiben. Sitemap-Generierung ist ein Rundungsfehler auf Ihren Vercel- und Supabase-Rechnungen.

Wenn Sie ein ähnliches großflächiges Projekt erstellen und Hilfe mit der Architektur benötigen, haben wir dies auf mehreren Client-Sites durchgeführt. Schauen Sie sich unsere Next.js-Entwicklungsfähigkeiten oder unsere Headless CMS-Entwicklungsarbeit an. Für Astro-basierte Sites mit ähnlichen Skalierungsanforderungen haben wir vergleichbare Lösungen mit Astro's Endpoint-Ansatz erstellt.

Der vollständige funktionierende Code ist als GitHub Gist verfügbar: alle Route Handler, die Supabase-Abfrage-Bibliothek und die next.config.ts-Rewrites. Wenn Ihr Projekt etwas Personalisiertes benötigt -- Multi-Tenant-Sitemaps, Echtzeit-Revalidierung oder Sitemaps für 1 Million+ Seiten -- kontaktieren Sie uns und wir werden es scopen.

FAQ

Wie viele URLs kann eine einzelne Sitemap-Datei enthalten?

Das Sitemap-Protokoll erlaubt maximal 50.000 URLs pro Datei und 50 MB unkomprimierte Dateigröße. Für Sites mit mehr als 50.000 Seiten benötigen Sie einen Sitemap-Index, der mehrere chunked Sitemap-Dateien referenziert. In der Praxis chunken die meisten Sitemap-Generatoren bei 45.000-50.000 URLs, um einen Sicherheitsspielraum zu lassen.

Sollte ich next-sitemap oder benutzerdefinierte Route Handler verwenden?

next-sitemap (v4+) ist großartig für einfachere Setups und verwaltet das automatische Chunking gut. Aber für 91K+ dynamische Seiten mit Content-Typ-spezifischen Prioritäten, lokalisierten Sitemaps mit hreflang und feingranularer ISR-Kontrolle geben Ihnen benutzerdefinierte Route Handler mehr Kontrolle. Wir gingen custom, denn wir brauchten unterschiedliche Revalidierungsintervalle pro Content-Typ und wollten, dass die Sitemap-Struktur unserem GSC-Debugging-Workflow entspricht.

Reiche ich jede einzelne Sitemap-Datei bei Google Search Console ein?

Nein. Reichen Sie nur die Sitemap-Index-URL ein (z. B. https://yourdomain.com/sitemap.xml). Google liest den Index und entdeckt automatisch alle referenzierten Sub-Sitemaps. Das Einreichen einzelner Dateien ist unnötig und verschmutzt Ihr GSC-Dashboard.

Wie oft sollten Sitemaps für große dynamische Sites neu generiert werden?

Für die meisten inhaltsreichen Sites ist stündliche Neugenerierung über ISR (revalidate = 3600) ein guter Standard. Wenn Sie Inhalte sehr häufig veröffentlichen, kombinieren Sie es mit On-Demand-Revalidierung, die durch Datenbank-Webhooks ausgelöst wird. Generieren Sie nicht bei jeder Anfrage neu -- das umgeht Caching und erhöht Supabase-Last unnötig.

Warum indexiert Google nicht alle meine Sitemap-URLs?

Die häufigsten Ursachen sind: versehentliche Noindex-Meta-Tags, robots.txt-Blockierung, schwache/doppelte Inhalte, Seiten ohne Links ohne interne Links und Crawl-Budget-Einschränkungen. Überprüfen Sie GSC's "Seiten"-Bericht unter "Warum Seiten nicht indexiert sind" auf spezifische Gründe. Für große Sites konzentrieren Sie sich auf die Verbesserung der internen Verknüpfung zu Seiten ohne Links -- dies ist oft der größte Hebel.

Beeinflusst der priority-Wert in Sitemaps tatsächlich Google-Rankings? Google hat öffentlich erklärt, dass es priority- und changefreq-Werte weitgehend ignoriert. Bing und andere Suchmaschinen verwenden sie jedoch. Das lastmod-Feld ist das wichtigste Sitemap-Signal -- stellen Sie sicher, dass es tatsächliche Inhaltsänderungen aus Ihrer Datenbank widerspiegelt, nicht den aktuellen Zeitstempel.

Wie gehe ich mit Supabase's 1.000-Zeilen-Limit für Sitemap-Abfragen um?

Verwenden Sie Supabase's .range(offset, offset + batchSize - 1)-Methode zum Paginieren in Batches von 1.000. Wiederholen Sie, bis Sie alle Zeilen für den aktuellen Sitemap-Chunk abgerufen haben. Verwenden Sie für Count-Only-Abfragen (verwendet im Sitemap-Index) .select('*', { count: 'exact', head: true }), die nur die Anzahl zurückgibt, ohne irgendwelche Zeilendaten zu übertragen.

Kann dieser Ansatz 500.000 oder 1 Million Seiten verarbeiten?

Ja, mit kleineren Anpassungen. Die chunked Architektur skaliert linear -- 1 Million Seiten würden etwa 20 Sub-Sitemaps produzieren. Die Hauptsorge wird Vercel's 60-Sekunden-Funktions-Timeout zum Generieren einzelner 50K-URL-Sitemaps. Wenn Sie dieses Limit erreichen, reduzieren Sie die Chunk-Größe auf 25.000 oder 10.000 URLs pro Datei. Das Sitemap-Protokoll erlaubt bis zu 50.000 Sitemaps in einem einzelnen Index, sodass Sie auf Index-Ebene kein Limit erreichen werden.