Zum Inhalt springen

Canonicals und Sitemaps, die nicht auseinanderdriften können

Ihre Sitemap listet Routen, die Ihr Router nicht mehr ausliefert, und Ihre Canonicals widersprechen beiden. Die Lösung ist strukturell - alle drei aus einer Quelle ableiten.

8 Min. Lesezeit

Drei Dinge beschreiben einem Crawler die URLs Ihrer Site: die Routen, die Ihre Anwendung tatsächlich ausliefert, das Canonical-Tag auf jeder Seite und Ihre Sitemap. In den meisten Codebasen werden diese an drei verschiedenen Orten, von drei verschiedenen Personen, zu drei verschiedenen Zeitpunkten gepflegt.

Sie driften auseinander. Sie driften immer.

Wie Drift aussieht

  • Die Sitemap listet /blog/old-post, das 404 liefert.
  • /pricing/ und /pricing geben beide 200 zurück, ohne Canonical dazwischen.
  • Das Canonical der deutschen Seite zeigt auf die englische URL, weil jemand den Metadaten-Block kopiert hat.
  • Eine neue Route ging vor drei Monaten live und steht in gar keiner Sitemap.
  • Die Sitemap enthält https://www.example.com/..., während die Site https://example.com/... ausliefert.

Nichts davon ist dramatisch. Zusammen bedeuten sie, dass der Crawler sein Budget für URLs ausgibt, die Ihnen egal sind, und Ranking-Signale auf Duplikate aufteilt.

Eine Quelle, drei Ausgaben

Die strukturelle Lösung: Machen Sie die URL zu einem berechneten Wert und schreiben Sie nie eine von Hand.

// site.config.ts - the only place a URL is constructed
export function absoluteUrl(path = '/'): string {
  return `${siteConfig.url}${path.startsWith('/') ? path : `/${path}`}`;
}
 
/**
 * Locale-aware pathname. `en` is the default locale and is served without a
 * prefix, so its canonical URLs stay at the site root.
 */
export function localizedPath(locale: Locale, path = '/'): string {
  const clean = path === '/' ? '' : path.startsWith('/') ? path : `/${path}`;
  return locale === defaultLocale ? clean || '/' : `/${locale}${clean}`;
}
 
export function localizedUrl(locale: Locale, path = '/'): string {
  return absoluteUrl(localizedPath(locale, path));
}

Jedes Canonical, jeder Sitemap-Eintrag und jede hreflang-Alternate ruft localizedUrl auf. Ändern Sie die Domain in einer Konstante, und alle drei folgen. Es gibt keine Gelegenheit, dass ein Schrägstrich am Ende in einer Ausgabe auftaucht und in einer anderen nicht, weil die Zeichenkette in genau einer Funktion gebaut wird.

Machen Sie das Canonical unmöglich vergessbar

Eine Seite sollte nicht ohne Canonical ausgeliefert werden können. Durchsetzbar wird das, indem Metadaten durch einen Helfer laufen, dessen Pflichtargumente alles enthalten, was ein Canonical braucht:

export async function generateMetadata({ params }): Promise<Metadata> {
  const { locale, slug } = await params;
  const doc = getDoc('blog', locale, slug);
  if (!doc) return {};
 
  return buildMetadata({
    locale,
    title: doc.title,
    description: doc.description,
    path: `/blog/${slug}`,          // canonical is derived from this
    availableLocales: doc.availableLocales,
    type: 'article',
  });
}

Wer eine Route hinzufügt, schreibt path, weil die Funktion sonst nicht durch den Typecheck kommt. Man kann das Canonical nicht vergessen und seine Form nicht falsch treffen, weil man nie eine URL schreibt.

Die Sitemap liest dieselbe Quelle wie die Seiten

Next.js erzeugt sitemap.xml aus app/sitemap.ts. Der Fehler ist, dort ein statisches Array hinzuschreiben.

export default function sitemap(): MetadataRoute.Sitemap {
  const entries: MetadataRoute.Sitemap = [];
 
  for (const collection of ['services', 'blog'] as const) {
    for (const locale of locales) {
      // Same helper the pages render from - the two cannot disagree.
      for (const doc of getDocs(collection, locale)) {
        const path = `/${collection}/${doc.slug}`;
        entries.push({
          url: localizedUrl(locale, path),
          lastModified: doc.updated ? new Date(doc.updated) : new Date(doc.date),
          alternates: alternatesFor(path, availableLocalesFor(collection, doc.slug)),
        });
      }
    }
  }
 
  return entries;
}

Nehmen Sie einen Artikel vom Netz, und er verlässt die Sitemap beim nächsten Build. Fügen Sie eine Übersetzung hinzu, und die Alternates wachsen. Kein Pflegeschritt, weil die Sitemap kein Dokument ist - sie ist eine Projektion.

lastModified ist eine Behauptung, keine Formalie

lastModified für jeden Eintrag auf new Date() zu setzen, teilt dem Crawler mit, dass sich Ihre ganze Site heute geändert hat, und das jeden Tag. Nach ein paar Zyklen glaubt er Ihnen nicht mehr. Nehmen Sie das echte Änderungsdatum des Dokuments.

Schrägstrich am Ende: entscheiden Sie sich und stehen Sie dazu

Next.js liefert standardmäßig ohne abschließenden Schrägstrich aus. Das ist in Ordnung. Nicht in Ordnung ist, beides auszuliefern.

// next.config.ts
const nextConfig: NextConfig = {
  trailingSlash: false,  // the default; state it so nobody 'fixes' it later
};

Bestätigen Sie dann, dass die andere Form weiterleitet statt aufzulösen:

curl -sI https://example.com/pricing/ | head -1
# HTTP/2 308   ← correct
# HTTP/2 200   ← two URLs for one page

Dieselbe Prüfung gilt für www gegen Apex und für http gegen https. Jedes sollte eine einzige dauerhafte Weiterleitung sein, keine zweite lebende Kopie.

Robots: indexieren Sie Ihre Previews nicht

Jedes Branch-Deployment ist eine vollständige Kopie Ihrer Site unter einer öffentlichen URL. Ist sie indexierbar, ist sie ein Duplikat Ihrer Produktions-Site unter anderem Hostnamen.

export default function robots(): MetadataRoute.Robots {
  // Preview and branch deployments must never be indexed.
  const isProduction =
    process.env.VERCEL_ENV === 'production' ||
    process.env.NEXT_PUBLIC_SITE_URL === siteConfig.url;
 
  if (!isProduction) {
    return { rules: [{ userAgent: '*', disallow: '/' }] };
  }
 
  return {
    rules: [{ userAgent: '*', allow: '/', disallow: ['/api/'] }],
    sitemap: absoluteUrl('/sitemap.xml'),
    host: siteConfig.url,
  };
}

Beachten Sie, dass das auch metadataBase repariert. Ein Preview-Deployment, das die Produktions-URL erbt, gibt Produktions-Canonicals von einem Staging-Host aus - was harmlos ist. Eines, das seinen eigenen Hostnamen in Canonicals ausgibt und dabei indexierbar ist, richtet aktiv Schaden an.

Eine Fünf-Minuten-Prüfung

# Canonical, on the page the crawler sees - not the inspector's hydrated DOM
curl -s https://example.com/de/blog/some-post | grep -E 'canonical|alternate'
 
# Sitemap parses, and the count matches what you expect
curl -s https://example.com/sitemap.xml | grep -c '<loc>'
 
# Every URL in the sitemap returns 200
curl -s https://example.com/sitemap.xml \
  | grep -oP '(?<=<loc>)[^<]+' \
  | xargs -P8 -I{} sh -c 'printf "%s %s\n" "$(curl -o /dev/null -sw "%{http_code}" "{}")" "{}"' \
  | grep -v '^200'

Der letzte Befehl sollte nichts ausgeben. Gibt er etwas aus, schickt Ihre Sitemap Crawler auf Seiten, die es nicht gibt - und sie wird das weiter tun, bis sich strukturell etwas daran ändert, wie die Sitemap entsteht.

Die strukturelle Änderung ist der ganze Punkt. Korrektes SEO ist keine Liste von Tags, an die man denken muss. Es ist eine Architektur, in der das falsche Tag nicht geschrieben werden kann.

Die mehrsprachige Fassung dieser Architektur fällt auf ihre eigene Weise aus, und hreflang ist die Stelle, an der es zuerst sichtbar wird. Die andere Hälfte derselben Disziplin ist Markup, das ein Crawler ohne Raten lesen kann. Wo die falschen Tags längst in Produktion stehen und jemand sie alle finden muss, ist Technical SEO das Projekt, das sie findet.

Verwandte Fragen

Soll die Sitemap jede Sprache einzeln auflisten?
Ein Eintrag pro URL mit den Alternativen darin, nicht ein Eintrag pro Sprache. Und kündigen Sie eine Sprache nur an, wo die Seite existiert, sonst schicken Sie Crawler auf Ihre eigenen 404er.
Müssen Canonical-Tags absolut sein?
Ja. Ein relativer Canonical wird gegen die aktuelle URL aufgelöst, wird dadurch versehentlich selbstreferenziell und ist als Signal wertlos.
Sollen Query-Parameter im Canonical bleiben?
Nur die, die den Inhalt ändern, etwa eine Seitenzahl. Tracking- und Filterparameter sollten auf die saubere URL kanonisieren, sonst vervielfachen Sie eine Seite über den Index.
Woher wissen wir, dass unsere Sitemap ehrlich ist?
Rufen Sie jede URL darin ab und prüfen Sie auf 200. Alles andere heißt, die Sitemap schickt Crawler auf Seiten, die es nicht gibt, und sie wird das weiter tun, bis sie aus derselben Quelle wie die Routen abgeleitet wird.

← Zurück zu allen Artikeln

Anrufen+1 848 272 7583WhatsApp+90 850 308 5436E-Mailinfo@codefacture.comKontaktseite