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();
}