From d092480cf37367eb2016cba12b16f3839cfcbf8c Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Tue, 3 Mar 2026 13:22:36 +0530 Subject: [PATCH] chore: Auto redirect to new domain --- src/hooks.server.ts | 6 - src/lib/components/McWrapper.svelte | 4 +- src/lib/components/Navbar.svelte | 7 +- .../components/migration/WelcomeBanner.svelte | 84 +++++++ .../migration/WhyWeMovedModal.svelte | 73 ++++++ src/lib/util/migration/domainMigration.ts | 233 ++++++++++++++++++ src/lib/util/promos/NewYear2026.svelte | 4 +- src/lib/util/stats.ts | 8 + src/lib/util/util.ts | 3 +- src/routes/+layout.svelte | 21 +- src/routes/edit/+page.svelte | 20 +- 11 files changed, 443 insertions(+), 20 deletions(-) delete mode 100644 src/hooks.server.ts create mode 100644 src/lib/components/migration/WelcomeBanner.svelte create mode 100644 src/lib/components/migration/WhyWeMovedModal.svelte create mode 100644 src/lib/util/migration/domainMigration.ts diff --git a/src/hooks.server.ts b/src/hooks.server.ts deleted file mode 100644 index 49af22b0..00000000 --- a/src/hooks.server.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Handle } from '@sveltejs/kit'; - -export const handle: Handle = async ({ event, resolve }) => { - const response = await resolve(event, {}); - return response; -}; diff --git a/src/lib/components/McWrapper.svelte b/src/lib/components/McWrapper.svelte index 50911d37..38f8848b 100644 --- a/src/lib/components/McWrapper.svelte +++ b/src/lib/components/McWrapper.svelte @@ -1,5 +1,6 @@ + showPopup={!isOnMermaidAI()}> {@render children()} diff --git a/src/lib/components/Navbar.svelte b/src/lib/components/Navbar.svelte index 32be0b79..7607fcde 100644 --- a/src/lib/components/Navbar.svelte +++ b/src/lib/components/Navbar.svelte @@ -21,9 +21,10 @@ interface Props { mobileToggle?: Snippet; children: Snippet; + hidePromotion?: boolean; } - let { children, mobileToggle }: Props = $props(); + let { children, mobileToggle, hidePromotion = false }: Props = $props(); type Links = ComponentProps['links']; @@ -39,7 +40,7 @@ } ]; - let activePromotion = $state(getActivePromotion()); + let activePromotion = $state(hidePromotion ? undefined : getActivePromotion()); const trackBannerClick = () => { if (!plausible || !activePromotion) { @@ -54,7 +55,7 @@ {#if activePromotion}
+ import { Button } from '$/components/ui/button'; + import { + dismissWelcomeBanner, + incrementWelcomeBannerVisitCount, + redirectToMermaidLive + } from '$/util/migration/domainMigration'; + import { logEvent } from '$/util/stats'; + import { onMount } from 'svelte'; + import CloseIcon from '~icons/material-symbols/close-rounded'; + import SwapIcon from '~icons/material-symbols/swap-horiz-rounded'; + import WhyWeMovedModal from './WhyWeMovedModal.svelte'; + + let visible = $state(true); + let showExplainer = $state(false); + + onMount(() => { + incrementWelcomeBannerVisitCount(); + }); + + const handleContinue = () => { + logEvent('bannerDismissed'); + dismissWelcomeBanner(); + visible = false; + }; + + const handleClose = () => { + // Just dismiss for this session, don't set permanent dismissal + logEvent('bannerDismissed'); + visible = false; + }; + + const handleGoBack = () => { + redirectToMermaidLive(); + }; + + const handleWhyDidWeMove = () => { + logEvent('explainerOpened'); + showExplainer = true; + }; + + +{#if visible} + +{/if} + + (showExplainer = v)} /> diff --git a/src/lib/components/migration/WhyWeMovedModal.svelte b/src/lib/components/migration/WhyWeMovedModal.svelte new file mode 100644 index 00000000..44c225e5 --- /dev/null +++ b/src/lib/components/migration/WhyWeMovedModal.svelte @@ -0,0 +1,73 @@ + + + + + + Why the new address? + + +
+

+ We're consolidating the Mermaid ecosystem under mermaid.ai — one home for the editor, docs, + and the commercial platform. +

+ +
+
+ +
+

Same privacy

+

+ Diagrams render client-side. No data sent to servers. +

+
+
+ +
+ +
+

Same open-source code

+

Identical codebase. MIT licensed.

+
+
+ +
+ +
+

Old links still work

+

+ All mermaid.live URLs with diagrams keep working. +

+
+
+
+
+ + + + + +
+
diff --git a/src/lib/util/migration/domainMigration.ts b/src/lib/util/migration/domainMigration.ts new file mode 100644 index 00000000..76dd22ea --- /dev/null +++ b/src/lib/util/migration/domainMigration.ts @@ -0,0 +1,233 @@ +import { logEvent } from '$/util/stats'; + +// Constants for the domain migration +const mermaidLiveDomain = 'mermaid.live'; +const mermaidAiDomain = 'mermaid.ai'; +const bypassCookieName = 'mermaid-stay'; +const welcomeBannerStorageKey = 'mermaid-welcome-banner-dismissed'; +const welcomeBannerVisitCountKey = 'mermaid-welcome-banner-visits'; +const redirectReferrerKey = 'mermaid-redirect-from'; + +/** + * Check if the current URL has pako data (diagram content). + * Pako data is in the hash fragment as #pako:... or as a legacy format. + */ +const hasPakoData = (): boolean => { + const hash = window.location.hash; + return ( + hash.includes('pako:') || hash.includes('base64:') || (hash.length > 10 && hash.startsWith('#')) + ); +}; + +/** + * Get the current domain + */ +const getCurrentDomain = (): string => { + return window.location.hostname; +}; + +/** + * Check if we're on mermaid.live + */ +const isOnMermaidLive = (): boolean => { + return ( + getCurrentDomain() === mermaidLiveDomain || getCurrentDomain().endsWith(`.${mermaidLiveDomain}`) + ); +}; + +/** + * Check if we're on mermaid.ai + */ +export const isOnMermaidAI = (): boolean => { + const domain = getCurrentDomain(); + return domain === mermaidAiDomain || domain.endsWith(`.${mermaidAiDomain}`); +}; + +/** + * Get a cookie value by name + */ +const getCookie = (name: string): string | null => { + const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); + return match ? match[2] : null; +}; + +/** + * Set a cookie + */ +const setCookie = (name: string, value: string, days: number): void => { + const expires = new Date(); + expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000); + document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`; +}; + +/** + * Check if the bypass cookie is set (user opted to stay on mermaid.live) + */ +const hasBypassCookie = (): boolean => { + return getCookie(bypassCookieName) === '1'; +}; + +/** + * Set the bypass cookie (user opted to stay on mermaid.live) + * Cookie is permanent (100 years) so user doesn't have to opt out repeatedly + */ +const setBypassCookie = (): void => { + setCookie(bypassCookieName, '1', 36500); // ~100 years +}; + +/** + * Redirect to mermaid.live with ?stay=1 to trigger bypass cookie on that domain. + * Used by escape hatch buttons in WelcomeBanner and WhyWeMovedModal. + */ +export const redirectToMermaidLive = (): void => { + logEvent('bannerEscapeClicked'); + window.location.href = `https://${mermaidLiveDomain}/edit?stay=1`; +}; + +/** + * Check if the URL has the ?stay=1 parameter (used for escape hatch) + */ +const hasStayParam = (): boolean => { + const params = new URLSearchParams(window.location.search); + return params.has('stay'); +}; + +/** + * Handle the ?stay=1 parameter: set bypass cookie and redirect to clean URL + */ +export const handleStayParam = (): boolean => { + if (hasStayParam()) { + logEvent('stayOnMermaidLive'); + setBypassCookie(); + // Remove the ?stay=1 param from URL + const url = new URL(window.location.href); + url.searchParams.delete('stay'); + window.history.replaceState({}, '', url.pathname + url.hash); + return true; + } + return false; +}; + +/** + * Get welcome banner visit count from localStorage + */ +const getWelcomeBannerVisitCount = (): number => { + const count = window.localStorage.getItem(welcomeBannerVisitCountKey); + return count ? parseInt(count, 10) : 0; +}; + +/** + * Increment welcome banner visit count + */ +export const incrementWelcomeBannerVisitCount = (): number => { + const count = getWelcomeBannerVisitCount() + 1; + window.localStorage.setItem(welcomeBannerVisitCountKey, count.toString()); + return count; +}; + +/** + * Check if welcome banner should be shown (shown for first 3 visits, then auto-hide) + */ +export const shouldShowWelcomeBanner = (): boolean => { + // Only show on mermaid.ai + if (!isOnMermaidAI()) return false; + // Check if banner was manually dismissed permanently + if (window.localStorage.getItem(welcomeBannerStorageKey) === 'true') return false; + // Check if redirect referrer was from mermaid.live + if (window.sessionStorage.getItem(redirectReferrerKey) !== 'mermaid.live') return false; + // Show for first 3 visits + return getWelcomeBannerVisitCount() < 3; +}; + +/** + * Mark redirect origin (call this when redirecting from mermaid.live) + */ +const setRedirectReferrer = (): void => { + window.sessionStorage.setItem(redirectReferrerKey, 'mermaid.live'); +}; + +/** + * Dismiss the welcome banner permanently + */ +export const dismissWelcomeBanner = (): void => { + window.localStorage.setItem(welcomeBannerStorageKey, 'true'); +}; + +// localStorage keys used by the app for user data +const userDataStorageKeys = [ + 'codeStore', // Current diagram code/state + 'manualHistoryStore', // Manual history entries + 'autoHistoryStore' // Auto history entries +]; + +/** + * Check if user has any stored data in localStorage. + * This includes saved diagrams, history entries, etc. + */ +const hasStoredUserData = (): boolean => { + for (const key of userDataStorageKeys) { + const value = window.localStorage.getItem(key); + if (value && value !== '[]' && value !== 'null' && value !== '{}') { + return true; + } + } + return false; +}; + +/** + * Check and perform domain migration redirect if needed. + * This should be called early in the app lifecycle. + * + * Redirect logic: + * - If on mermaid.live with no pako data in URL, no bypass cookie, and no stored data → redirect to mermaid.ai/live + * - If URL has pako data → stay (user followed a shared link) + * - If bypass cookie is set → stay (user opted out of redirect) + * - If user has stored data in localStorage → stay (user has history/diagrams saved locally) + * + * @returns true if a redirect was triggered, false otherwise + */ +export const checkAndRedirectIfNeeded = (): boolean => { + // Only redirect from mermaid.live + if (!isOnMermaidLive()) return false; + + // Don't redirect if user has bypass cookie + if (hasBypassCookie()) return false; + + // Don't redirect if URL has pako data (shared link) + if (hasPakoData()) return false; + + // Don't redirect if user has stored data in localStorage (history, saved diagrams, etc.) + if (hasStoredUserData()) return false; + + // Perform the redirect to mermaid.ai/live + // Set a sessionStorage flag so we know this user came from mermaid.live + // (sessionStorage doesn't transfer cross-domain, so we'll use a query param instead) + const currentPath = window.location.pathname; + const targetPath = currentPath.replace(/^\/?/, '/live/'); + const redirectUrl = `https://${mermaidAiDomain}${targetPath}?from=mermaid.live`; + + window.location.href = redirectUrl; + return true; +}; + +/** + * Handle incoming redirect from mermaid.live to mermaid.ai. + * Checks for the ?from=mermaid.live param and sets session storage. + */ +export const handleIncomingRedirect = (): void => { + // Only on mermaid.ai + if (!isOnMermaidAI()) return; + + const params = new URLSearchParams(window.location.search); + if (params.get('from') === 'mermaid.live') { + // Mark that this session originated from a redirect + setRedirectReferrer(); + + // Clean up the URL + params.delete('from'); + const cleanSearch = params.toString(); + const newUrl = + window.location.pathname + (cleanSearch ? `?${cleanSearch}` : '') + window.location.hash; + window.history.replaceState({}, '', newUrl); + } +}; diff --git a/src/lib/util/promos/NewYear2026.svelte b/src/lib/util/promos/NewYear2026.svelte index d9d5112e..e213b330 100644 --- a/src/lib/util/promos/NewYear2026.svelte +++ b/src/lib/util/promos/NewYear2026.svelte @@ -1,6 +1,6 @@