marketing-host.ts3.4 KBView on GitHub /**
* Marketing host vs app host.
*
* Cedar's marketing pages and the signed-in app are one React Router build, and for a
* long time they answered on one hostname (mail.cedarcopilot.com). The marketing site
* now gets its own origin — cedarcopilot.com — with mail.* reserved for the app.
*
* Both origins still serve the same bundle, so the split is enforced here: each origin
* bounces the paths that belong to the other one.
*
* Everything is gated on VITE_PUBLIC_MARKETING_URL. Until that is set to a host that
* actually differs from the app host, every helper reports "not split" and routing
* behaves exactly as it did before — so this is safe to ship ahead of the DNS cutover.
*/
const configuredMarketingUrl = import.meta.env.VITE_PUBLIC_MARKETING_URL as string | undefined;
const configuredAppUrl = import.meta.env.VITE_PUBLIC_APP_URL as string | undefined;
function hostOf(url: string | undefined): string | null {
if (!url) return null;
try {
return new URL(url).host.toLowerCase();
} catch {
return null;
}
}
const marketingHost = hostOf(configuredMarketingUrl);
const appHost = hostOf(configuredAppUrl);
/** True once the marketing site has an origin of its own, distinct from the app's. */
export function hasSplitHosts(): boolean {
return Boolean(marketingHost && appHost && marketingHost !== appHost);
}
function currentHost(): string | null {
if (typeof window === 'undefined') return null;
return window.location.host.toLowerCase();
}
/**
* True when the page is being served from the marketing origin. `www.` is treated as
* the marketing host too, so a visitor who typed www doesn't get bounced to the app.
*/
export function isMarketingHost(): boolean {
if (!hasSplitHosts()) return false;
const host = currentHost();
if (!host) return false;
return host === marketingHost || host === `www.${marketingHost}`;
}
/** True when the page is being served from the app origin while a split is configured. */
export function isAppHost(): boolean {
if (!hasSplitHosts()) return false;
return currentHost() === appHost;
}
/**
* Paths that belong to the marketing site. Prefix entries match the path and anything
* beneath it; `/` is matched exactly so it doesn't swallow every route.
*/
const MARKETING_PATHS = ['/pricing', '/blog', '/bookdemo', '/legal', '/roadmap', '/hr'];
export function isMarketingPath(pathname: string): boolean {
if (pathname === '/') return true;
return MARKETING_PATHS.some(
(prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`),
);
}
/** Absolute URL for `path` on the marketing origin, or null when there is no split. */
export function marketingUrl(path: string): string | null {
if (!configuredMarketingUrl || !hasSplitHosts()) return null;
return new URL(path, configuredMarketingUrl).toString();
}
/**
* Leave the current origin for `url` on the other one.
*
* Two origins are two documents, so crossing between them takes a full-page navigation
* rather than a router redirect. `replace` keeps the origin being left out of the back
* stack — otherwise Back lands on a page whose whole job is to bounce straight out again.
*/
export function leaveForOrigin(url: string): void {
window.location.replace(url);
}
/** Absolute URL for `path` on the app origin, or null when there is no split. */
export function appUrl(path: string): string | null {
if (!configuredAppUrl || !hasSplitHosts()) return null;
return new URL(path, configuredAppUrl).toString();
}