Files
craftvia/src/lib/marketing/hosts.ts
T
msolarczekandClaude Opus 5 4d5be72ebb Website und App auf getrennten Domains (craftvia.de / app.craftvia.de)
Der Proxy trennt nach Host, sobald MARKETING_HOST gesetzt ist: craftvia.de zeigt nur die
Produktseiten und leitet Login, Testphase, App und API auf die App-Domain aus AUTH_URL um;
www leitet auf die Hauptdomain; auf der App-Domain landet / ohne Sitzung beim Login und
Produktseiten gehen zurück zur Website. Interne Aufrufe (Healthcheck) bleiben unverändert.
Links bleiben relativ, damit keine Domain im Image steckt.

Dazu Test scripts/test-host-routing.ts, Env-Vorlagen mit MARKETING_HOST und Registry,
scripts/generate-coolify-env.sh für lokal erzeugte Geheimnisse und DEPLOY.md §2.1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 09:08:47 +02:00

59 lines
2.7 KiB
TypeScript

/**
* Domain-Trennung Website ↔ App: `craftvia.de` zeigt nur die Produktseiten, `app.craftvia.de` die
* Anwendung. Aktiv nur, wenn `MARKETING_HOST` gesetzt ist; die App-Domain kommt aus `AUTH_URL`.
* Ohne `MARKETING_HOST` (lokal, Tests) läuft alles wie bisher auf einem Host.
*
* Links der Produktseiten bleiben relativ (/login, /testen): der Proxy leitet sie von der
* Website-Domain auf die App-Domain um. So wird keine Domain beim Image-Bau festgeschrieben.
*/
export const MARKETING_PATHS = ["/", "/funktionen", "/sicherheit", "/preise", "/impressum"] as const;
export type HostSplit = { marketingHost: string; appHost: string; appOrigin: string };
export type HostDecision = { kind: "next" } | { kind: "redirect"; location: string; status: 307 | 308 };
export function hostSplit(env: Record<string, string | undefined> = process.env): HostSplit | null {
const marketingHost = env.MARKETING_HOST?.trim()
.toLowerCase()
.replace(/^https?:\/\//, "")
.replace(/[/:].*$/, "");
const authUrl = env.AUTH_URL?.trim();
if (!marketingHost || !authUrl) return null;
let app: URL;
try {
app = new URL(authUrl);
} catch {
return null;
}
const appHost = app.hostname.toLowerCase();
if (appHost === marketingHost) return null;
return { marketingHost, appHost, appOrigin: app.origin };
}
export function isMarketingPath(pathname: string): boolean {
return MARKETING_PATHS.some((p) => pathname === p || (p !== "/" && pathname.startsWith(p + "/")));
}
export function routeByHost(input: { host: string; pathname: string; search: string; hasSession: boolean }, split: HostSplit): HostDecision {
const host = input.host.toLowerCase().split(",")[0].trim().replace(/:\d+$/, "");
const target = input.pathname + input.search;
const marketingOrigin = `https://${split.marketingHost}`;
// www → Hauptdomain
if (host === `www.${split.marketingHost}`) return { kind: "redirect", location: marketingOrigin + target, status: 308 };
// Website-Domain: nur Produktseiten, alles andere (Login, Testphase, App, API) gehört zur App
if (host === split.marketingHost) {
return isMarketingPath(input.pathname) ? { kind: "next" } : { kind: "redirect", location: split.appOrigin + target, status: 308 };
}
// Interne Aufrufe (Healthcheck über 127.0.0.1, Worker) bleiben unverändert
if (host !== split.appHost) return { kind: "next" };
// App-Domain: Produktseiten wohnen auf der Website, die Startseite ist der Login
if (input.pathname !== "/" && isMarketingPath(input.pathname)) return { kind: "redirect", location: marketingOrigin + target, status: 308 };
if (input.pathname === "/" && !input.hasSession) return { kind: "redirect", location: `${split.appOrigin}/login`, status: 307 };
return { kind: "next" };
}