fix: Sitemap, Canonicals und JSON-LD folgen der echten Domain

Die Adresse der Seite war an zwei Stellen gepflegt: E-Mails nutzten
NEXT_PUBLIC_SITE_URL, alles SEO-Relevante dagegen ein fest verdrahtetes
"https://" + siteConfig.domain (16 Stellen, darunter eine roh eingetippte
"https://wabenhain.com" im Produkt-JSON-LD).

Laeuft der Shop unter einer anderen Domain als wabenhain.com — Staging,
Vorschau-Host, spaeterer Domainwechsel, der im k8s-Ingress hinterlegte Host
— zeigten damit sitemap.xml, robots.txt, saemtliche Canonical-Tags und alle
strukturierten Daten geschlossen auf eine fremde Adresse. Google verwirft
eine Sitemap mit fremden Hosts, und ein Canonical auf eine andere Domain
weist die Indexierung aktiv von der echten Seite weg.

Jetzt eine einzige Quelle: getSiteUrl() in lib/url.ts, mit
siteConfig.domain als Rueckfallebene, damit eine vergessene
Umgebungsvariable nicht "http://localhost:3000" in einer oeffentlichen
Sitemap landen laesst. getBaseUrl() bleibt als Alias fuer den in
E-Mail-Code eingebuergerten Namen und liefert denselben Wert.

robots.ts bekommt dabei "force-dynamic" (wie sitemap.ts es schon hatte):
ohne das rendert Next.js die Datei beim Build statisch vor und backt den
Wert ein, den NEXT_PUBLIC_SITE_URL genau dann hatte. Im Docker-Build ist
die Variable nicht gesetzt — die Sitemap-Zeile waere also dauerhaft auf dem
Rueckfallwert eingefroren. Gegen einen laufenden Produktions-Build
geprueft: robots.txt, sitemap.xml, Canonical-Tags und JSON-LD folgen jetzt
alle dem zur Laufzeit gesetzten Wert.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5 committed 2026-09-15 10:00:59 +02:00
1 parent 96bb3d9c50
commit d3020cd4da
8 files changed
+112 -25

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
import { describe, it, expect, afterEach } from "vitest";
import { getSiteUrl, getBaseUrl } from "./url";
import { siteConfig } from "./site-config";
const original = process.env.NEXT_PUBLIC_SITE_URL;
afterEach(() => {
if (original === undefined) delete process.env.NEXT_PUBLIC_SITE_URL;
else process.env.NEXT_PUBLIC_SITE_URL = original;
});
describe("getSiteUrl", () => {
it("nutzt NEXT_PUBLIC_SITE_URL, wenn gesetzt", () => {
process.env.NEXT_PUBLIC_SITE_URL = "https://shop.beispiel.de";
expect(getSiteUrl()).toBe("https://shop.beispiel.de");
});
it("entfernt abschliessende Schraegstriche, damit keine doppelten entstehen", () => {
process.env.NEXT_PUBLIC_SITE_URL = "https://shop.beispiel.de//";
// Zusammengesetzt ergaebe das sonst "https://shop.beispiel.de//shop" —
// fuer Google eine andere URL als die kanonische Variante.
expect(`${getSiteUrl()}/shop`).toBe("https://shop.beispiel.de/shop");
});
it("faellt auf die konfigurierte Domain zurueck statt auf localhost", () => {
delete process.env.NEXT_PUBLIC_SITE_URL;
// Eine vergessene Umgebungsvariable darf nicht dazu fuehren, dass
// "http://localhost:3000" in einer oeffentlichen Sitemap landet.
expect(getSiteUrl()).toBe(`https://${siteConfig.domain}`);
});
it("ignoriert einen Wert, der nur aus Leerzeichen besteht", () => {
process.env.NEXT_PUBLIC_SITE_URL = " ";
expect(getSiteUrl()).toBe(`https://${siteConfig.domain}`);
});
it("liefert unter getBaseUrl denselben Wert — E-Mails und SEO duerfen nicht auseinanderlaufen", () => {
process.env.NEXT_PUBLIC_SITE_URL = "https://shop.beispiel.de";
expect(getBaseUrl()).toBe(getSiteUrl());
});
});
+32 -4
View File
@@ -1,8 +1,36 @@
import { siteConfig } from "@/lib/site-config";
/**
* Basis-URL fuer absolute Links in E-Mails (Bestellbestaetigung,
* Newsletter-Opt-in, Passwort-Reset). Webhooks/Server Actions haben keinen
* verlaesslichen Request-Origin, deshalb NEXT_PUBLIC_SITE_URL statt Header.
* Oeffentliche Basis-URL der Seite, ohne abschliessenden Schraegstrich
* (z.B. "https://wabenhain.com").
*
* EINZIGE Quelle fuer absolute Links — sowohl fuer E-Mails (Bestellbestaetigung,
* Newsletter-Opt-in, Passwort-Reset) als auch fuer alles, was Suchmaschinen
* sehen: metadataBase/Canonicals (app/layout.tsx), sitemap.xml, robots.txt und
* saemtliche JSON-LD-Bloecke.
*
* Vorher war das auf zwei Quellen verteilt: E-Mails nutzten
* NEXT_PUBLIC_SITE_URL, SEO-relevante Stellen dagegen ein fest verdrahtetes
* "https://" + siteConfig.domain. Laeuft die Seite unter einer anderen Domain
* (Staging, Vorschau-Host, spaeterer Domainwechsel), zeigten Sitemap,
* Canonical-Tags und strukturierte Daten damit geschlossen auf eine fremde
* Adresse: Google verwirft eine Sitemap mit fremden Hosts, und Canonicals auf
* eine andere Domain weisen die Indexierung aktiv von der echten Seite weg.
*
* siteConfig.domain bleibt als Rueckfallebene erhalten, damit eine vergessene
* Umgebungsvariable in Produktion nicht zu "http://localhost:3000" in einer
* oeffentlichen Sitemap fuehrt.
*/
export function getSiteUrl(): string {
const configured = process.env.NEXT_PUBLIC_SITE_URL?.trim();
if (configured) return configured.replace(/\/+$/, "");
return `https://${siteConfig.domain}`;
}
/**
* Alias von getSiteUrl() unter dem in E-Mail-Code eingebuergerten Namen.
* Beide liefern denselben Wert — der Unterschied ist rein sprachlich.
*/
export function getBaseUrl(): string {
return process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000";
return getSiteUrl();
}