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>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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" };
|
||||
}
|
||||
Reference in New Issue
Block a user