feat(web): SEO optimization and auth flow improvements for landing pages

- Split landing pages into Server/Client Components to enable Next.js metadata exports
- Add robots.ts, sitemap.ts, JSON-LD structured data, OpenGraph and viewport config
- Fix i18n hydration mismatch: detect locale server-side via cookie/Accept-Language header
- Replace localStorage with cookie for locale persistence (SSR-readable)
- Dynamic <html lang> based on locale cookie
- Optimize images with next/image (avif/webp formats, quality config)
- Add /homepage route as always-visible landing page (no auth redirect)
- Auth-aware CTA buttons: show "Dashboard"/"进入工作台" for logged-in users
- Login page redirects authenticated users to dashboard
- Unify logout/401 redirect to "/" instead of "/login"
- Fix title template to avoid double "Multica" suffix on homepage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-04-01 16:54:11 +08:00
parent c6e67bd41e
commit 3bd504fe5f
24 changed files with 392 additions and 176 deletions

View file

@ -46,11 +46,20 @@ function redirectToCliCallback(
function LoginPageContent() {
const router = useRouter();
const user = useAuthStore((s) => s.user);
const isLoading = useAuthStore((s) => s.isLoading);
const sendCode = useAuthStore((s) => s.sendCode);
const verifyCode = useAuthStore((s) => s.verifyCode);
const hydrateWorkspace = useWorkspaceStore((s) => s.hydrateWorkspace);
const searchParams = useSearchParams();
// Already authenticated — redirect to dashboard
useEffect(() => {
if (!isLoading && user && !searchParams.get("cli_callback")) {
router.replace(searchParams.get("next") || "/issues");
}
}, [isLoading, user, router, searchParams]);
const [step, setStep] = useState<"email" | "code" | "cli_confirm">("email");
const [email, setEmail] = useState("");
const [code, setCode] = useState("");

View file

@ -76,9 +76,9 @@ export function AppSidebar() {
const unreadCount = useInboxStore((s) => s.unreadCount());
const logout = () => {
router.push("/");
authLogout();
useWorkspaceStore.getState().clearWorkspace();
router.push("/login");
};
return (

View file

@ -22,7 +22,7 @@ export default function DashboardLayout({
useEffect(() => {
if (!isLoading && !user) {
router.push("/login");
router.push("/");
}
}, [user, isLoading, router]);

View file

@ -1,62 +1,21 @@
"use client";
import type { Metadata } from "next";
import { AboutPageClient } from "@/features/landing/components/about-page-client";
import Link from "next/link";
import { LandingHeader } from "@/features/landing/components/landing-header";
import { LandingFooter } from "@/features/landing/components/landing-footer";
import { GitHubMark, githubUrl } from "@/features/landing/components/shared";
import { useLocale } from "@/features/landing/i18n";
export const metadata: Metadata = {
title: "About",
description:
"Learn about Multica — multiplexed information and computing agent. An open-source AI-native task management platform.",
openGraph: {
title: "About Multica",
description:
"The story behind Multica and why we're building AI-native task management.",
url: "/about",
},
alternates: {
canonical: "/about",
},
};
export default function AboutPage() {
const { t } = useLocale();
const n = t.about.nameLine;
return (
<>
<LandingHeader variant="light" />
<main className="bg-white text-[#0a0d12]">
<div className="mx-auto max-w-[720px] px-4 py-16 sm:px-6 sm:py-20 lg:py-24">
<h1 className="font-[family-name:var(--font-serif)] text-[2.6rem] leading-[1.05] tracking-[-0.03em] sm:text-[3.4rem]">
{t.about.title}
</h1>
<div className="mt-8 space-y-6 text-[15px] leading-[1.8] text-[#0a0d12]/70 sm:text-[16px]">
<p>
{n.prefix}
<strong className="font-semibold text-[#0a0d12]">
{n.mul}
</strong>
{n.tiplexed}
<strong className="font-semibold text-[#0a0d12]">
{n.i}
</strong>
{n.nformationAnd}
<strong className="font-semibold text-[#0a0d12]">
{n.c}
</strong>
{n.omputing}
<strong className="font-semibold text-[#0a0d12]">
{n.a}
</strong>
{n.gent}
</p>
{t.about.paragraphs.map((p, i) => (
<p key={i}>{p}</p>
))}
</div>
<div className="mt-12">
<Link
href={githubUrl}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2.5 rounded-[12px] bg-[#0a0d12] px-5 py-3 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0d12]/88"
>
<GitHubMark className="size-4" />
{t.about.cta}
</Link>
</div>
</div>
</main>
<LandingFooter />
</>
);
return <AboutPageClient />;
}

View file

@ -1,55 +1,20 @@
"use client";
import type { Metadata } from "next";
import { ChangelogPageClient } from "@/features/landing/components/changelog-page-client";
import { LandingHeader } from "@/features/landing/components/landing-header";
import { LandingFooter } from "@/features/landing/components/landing-footer";
import { useLocale } from "@/features/landing/i18n";
export const metadata: Metadata = {
title: "Changelog",
description:
"See what's new in Multica — latest features, improvements, and fixes.",
openGraph: {
title: "Changelog | Multica",
description: "Latest updates and releases from Multica.",
url: "/changelog",
},
alternates: {
canonical: "/changelog",
},
};
export default function ChangelogPage() {
const { t } = useLocale();
return (
<>
<LandingHeader variant="light" />
<main className="bg-white text-[#0a0d12]">
<div className="mx-auto max-w-[720px] px-4 py-16 sm:px-6 sm:py-20 lg:py-24">
<h1 className="font-[family-name:var(--font-serif)] text-[2.6rem] leading-[1.05] tracking-[-0.03em] sm:text-[3.4rem]">
{t.changelog.title}
</h1>
<p className="mt-4 text-[15px] leading-7 text-[#0a0d12]/60 sm:text-[16px]">
{t.changelog.subtitle}
</p>
<div className="mt-16 space-y-16">
{t.changelog.entries.map((release) => (
<div key={release.version} className="relative">
<div className="flex items-baseline gap-3">
<span className="text-[13px] font-semibold tabular-nums">
v{release.version}
</span>
<span className="text-[13px] text-[#0a0d12]/40">
{release.date}
</span>
</div>
<h2 className="mt-2 text-[20px] font-semibold leading-snug sm:text-[22px]">
{release.title}
</h2>
<ul className="mt-4 space-y-2">
{release.changes.map((change) => (
<li
key={change}
className="flex items-start gap-2.5 text-[14px] leading-[1.7] text-[#0a0d12]/60 sm:text-[15px]"
>
<span className="mt-2.5 h-1 w-1 shrink-0 rounded-full bg-[#0a0d12]/30" />
{change}
</li>
))}
</ul>
</div>
))}
</div>
</div>
</main>
<LandingFooter />
</>
);
return <ChangelogPageClient />;
}

View file

@ -0,0 +1,21 @@
import type { Metadata } from "next";
import { MulticaLanding } from "@/features/landing/components/multica-landing";
export const metadata: Metadata = {
title: "Homepage",
description:
"Multica — open-source platform that turns coding agents into real teammates. Assign tasks, track progress, compound skills.",
openGraph: {
title: "Multica — AI-Native Task Management",
description:
"Manage your human + agent workforce in one place.",
url: "/homepage",
},
alternates: {
canonical: "/homepage",
},
};
export default function HomepagePage() {
return <MulticaLanding />;
}

View file

@ -1,5 +1,7 @@
import { cookies, headers } from "next/headers";
import { Instrument_Serif, Noto_Serif_SC } from "next/font/google";
import { LocaleProvider } from "@/features/landing/i18n";
import type { Locale } from "@/features/landing/i18n";
const instrumentSerif = Instrument_Serif({
subsets: ["latin"],
@ -13,14 +15,61 @@ const notoSerifSC = Noto_Serif_SC({
variable: "--font-serif-zh",
});
export default function LandingLayout({
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
name: "Multica",
url: "https://www.multica.ai",
sameAs: ["https://github.com/multica-ai/multica"],
},
{
"@type": "SoftwareApplication",
name: "Multica",
applicationCategory: "ProjectManagement",
operatingSystem: "Web",
description:
"AI-native task management platform that turns coding agents into real teammates.",
offers: {
"@type": "Offer",
price: "0",
priceCurrency: "USD",
},
},
],
};
async function getInitialLocale(): Promise<Locale> {
// 1. User's explicit preference (cookie set when they switch language)
const cookieStore = await cookies();
const stored = cookieStore.get("multica-locale")?.value;
if (stored === "en" || stored === "zh") return stored;
// 2. Detect from Accept-Language header
const headersList = await headers();
const acceptLang = headersList.get("accept-language") ?? "";
if (acceptLang.includes("zh")) return "zh";
return "en";
}
export default async function LandingLayout({
children,
}: {
children: React.ReactNode;
}) {
const initialLocale = await getInitialLocale();
return (
<div className={`${instrumentSerif.variable} ${notoSerifSC.variable} h-full overflow-x-hidden overflow-y-auto bg-white`}>
<LocaleProvider>{children}</LocaleProvider>
</div>
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<div className={`${instrumentSerif.variable} ${notoSerifSC.variable} h-full overflow-x-hidden overflow-y-auto bg-white`}>
<LocaleProvider initialLocale={initialLocale}>{children}</LocaleProvider>
</div>
</>
);
}

View file

@ -1,31 +1,23 @@
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useAuthStore } from "@/features/auth";
import { useNavigationStore } from "@/features/navigation";
import type { Metadata } from "next";
import { MulticaLanding } from "@/features/landing/components/multica-landing";
import { MulticaIcon } from "@/components/multica-icon";
export const metadata: Metadata = {
title: {
absolute: "Multica — AI-Native Task Management",
},
description:
"Open-source platform that turns coding agents into real teammates. Assign tasks, track progress, compound skills.",
openGraph: {
title: "Multica — AI-Native Task Management",
description:
"Manage your human + agent workforce in one place.",
url: "/",
},
alternates: {
canonical: "/",
},
};
export default function LandingPage() {
const router = useRouter();
const user = useAuthStore((s) => s.user);
const isLoading = useAuthStore((s) => s.isLoading);
useEffect(() => {
if (!isLoading && user) {
const lastPath = useNavigationStore.getState().lastPath;
router.replace(lastPath);
}
}, [isLoading, user, router]);
if (isLoading || user) {
return (
<div className="flex h-screen items-center justify-center">
<MulticaIcon className="size-6" />
</div>
);
}
return <MulticaLanding />;
}

View file

@ -1,4 +1,5 @@
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
import { cookies } from "next/headers";
import { Geist, Geist_Mono } from "next/font/google";
import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/sonner";
@ -11,23 +12,49 @@ import "./globals.css";
const geist = Geist({ subsets: ["latin"], variable: "--font-sans" });
const geistMono = Geist_Mono({ subsets: ["latin"], variable: "--font-mono" });
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#ffffff" },
{ media: "(prefers-color-scheme: dark)", color: "#05070b" },
],
};
export const metadata: Metadata = {
title: "Multica",
description: "AI-native task management",
metadataBase: new URL("https://www.multica.ai"),
title: {
default: "Multica — AI-Native Task Management",
template: "%s | Multica",
},
description:
"Open-source platform that turns coding agents into real teammates. Assign tasks, track progress, compound skills.",
icons: {
icon: [{ url: "/favicon.svg", type: "image/svg+xml" }],
shortcut: ["/favicon.svg"],
},
openGraph: {
type: "website",
siteName: "Multica",
locale: "en_US",
},
twitter: {
card: "summary_large_image",
},
};
export default function RootLayout({
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const cookieStore = await cookies();
const locale = cookieStore.get("multica-locale")?.value;
const lang = locale === "zh" ? "zh" : "en";
return (
<html
lang="en"
lang={lang}
suppressHydrationWarning
className={cn("antialiased font-sans h-full", geist.variable, geistMono.variable)}
>

28
apps/web/app/robots.ts Normal file
View file

@ -0,0 +1,28 @@
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
const baseUrl = "https://www.multica.ai";
return {
rules: [
{
userAgent: "*",
allow: ["/", "/about", "/changelog"],
disallow: [
"/api/",
"/ws",
"/auth/",
"/issues",
"/board",
"/inbox",
"/agents",
"/settings",
"/my-issues",
"/runtimes",
"/skills",
],
},
],
sitemap: `${baseUrl}/sitemap.xml`,
};
}

26
apps/web/app/sitemap.ts Normal file
View file

@ -0,0 +1,26 @@
import type { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = "https://www.multica.ai";
return [
{
url: baseUrl,
lastModified: new Date("2026-04-01"),
changeFrequency: "weekly",
priority: 1.0,
},
{
url: `${baseUrl}/about`,
lastModified: new Date("2026-04-01"),
changeFrequency: "monthly",
priority: 0.7,
},
{
url: `${baseUrl}/changelog`,
lastModified: new Date("2026-04-01"),
changeFrequency: "weekly",
priority: 0.6,
},
];
}

View file

@ -0,0 +1,62 @@
"use client";
import Link from "next/link";
import { LandingHeader } from "./landing-header";
import { LandingFooter } from "./landing-footer";
import { GitHubMark, githubUrl } from "./shared";
import { useLocale } from "../i18n";
export function AboutPageClient() {
const { t } = useLocale();
const n = t.about.nameLine;
return (
<>
<LandingHeader variant="light" />
<main className="bg-white text-[#0a0d12]">
<div className="mx-auto max-w-[720px] px-4 py-16 sm:px-6 sm:py-20 lg:py-24">
<h1 className="font-[family-name:var(--font-serif)] text-[2.6rem] leading-[1.05] tracking-[-0.03em] sm:text-[3.4rem]">
{t.about.title}
</h1>
<div className="mt-8 space-y-6 text-[15px] leading-[1.8] text-[#0a0d12]/70 sm:text-[16px]">
<p>
{n.prefix}
<strong className="font-semibold text-[#0a0d12]">
{n.mul}
</strong>
{n.tiplexed}
<strong className="font-semibold text-[#0a0d12]">
{n.i}
</strong>
{n.nformationAnd}
<strong className="font-semibold text-[#0a0d12]">
{n.c}
</strong>
{n.omputing}
<strong className="font-semibold text-[#0a0d12]">
{n.a}
</strong>
{n.gent}
</p>
{t.about.paragraphs.map((p, i) => (
<p key={i}>{p}</p>
))}
</div>
<div className="mt-12">
<Link
href={githubUrl}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2.5 rounded-[12px] bg-[#0a0d12] px-5 py-3 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0d12]/88"
>
<GitHubMark className="size-4" />
{t.about.cta}
</Link>
</div>
</div>
</main>
<LandingFooter />
</>
);
}

View file

@ -0,0 +1,55 @@
"use client";
import { LandingHeader } from "./landing-header";
import { LandingFooter } from "./landing-footer";
import { useLocale } from "../i18n";
export function ChangelogPageClient() {
const { t } = useLocale();
return (
<>
<LandingHeader variant="light" />
<main className="bg-white text-[#0a0d12]">
<div className="mx-auto max-w-[720px] px-4 py-16 sm:px-6 sm:py-20 lg:py-24">
<h1 className="font-[family-name:var(--font-serif)] text-[2.6rem] leading-[1.05] tracking-[-0.03em] sm:text-[3.4rem]">
{t.changelog.title}
</h1>
<p className="mt-4 text-[15px] leading-7 text-[#0a0d12]/60 sm:text-[16px]">
{t.changelog.subtitle}
</p>
<div className="mt-16 space-y-16">
{t.changelog.entries.map((release) => (
<div key={release.version} className="relative">
<div className="flex items-baseline gap-3">
<span className="text-[13px] font-semibold tabular-nums">
v{release.version}
</span>
<span className="text-[13px] text-[#0a0d12]/40">
{release.date}
</span>
</div>
<h2 className="mt-2 text-[20px] font-semibold leading-snug sm:text-[22px]">
{release.title}
</h2>
<ul className="mt-4 space-y-2">
{release.changes.map((change) => (
<li
key={change}
className="flex items-start gap-2.5 text-[14px] leading-[1.7] text-[#0a0d12]/60 sm:text-[15px]"
>
<span className="mt-2.5 h-1 w-1 shrink-0 rounded-full bg-[#0a0d12]/30" />
{change}
</li>
))}
</ul>
</div>
))}
</div>
</div>
</main>
<LandingFooter />
</>
);
}

View file

@ -1,6 +1,7 @@
"use client";
import { useEffect, useRef, useState } from "react";
import Image from "next/image";
import {
Bot,
Brain,
@ -1047,8 +1048,16 @@ export function FeaturesSection() {
{/* Visual */}
<div className="mt-14 sm:mt-18">
{feature.visual ? (
<div className="relative overflow-hidden rounded-sm" style={{ backgroundImage: `url(${feature.bgImage ?? "/images/feature-bg.jpg"})`, backgroundSize: "cover", backgroundPosition: "center" }}>
<div className="px-4 py-8 sm:px-6 sm:py-12 lg:px-8 lg:py-16">
<div className="relative overflow-hidden rounded-sm">
<Image
src={feature.bgImage ?? "/images/feature-bg.jpg"}
alt=""
fill
className="object-cover object-center"
sizes="(max-width: 1320px) 100vw, 1320px"
quality={80}
/>
<div className="relative px-4 py-8 sm:px-6 sm:py-12 lg:px-8 lg:py-16">
<feature.visual />
</div>
</div>

View file

@ -1,11 +1,13 @@
"use client";
import Link from "next/link";
import { useAuthStore } from "@/features/auth";
import { useLocale } from "../i18n";
import { GitHubMark, githubUrl, heroButtonClassName } from "./shared";
export function HowItWorksSection() {
const { t } = useLocale();
const user = useAuthStore((s) => s.user);
return (
<section id="how-it-works" className="bg-[#05070b] text-white">
@ -39,8 +41,8 @@ export function HowItWorksSection() {
</div>
<div className="mt-14 flex flex-wrap items-center gap-4">
<Link href="/login" className={heroButtonClassName("solid")}>
{t.howItWorks.cta}
<Link href={user ? "/issues" : "/login"} className={heroButtonClassName("solid")}>
{user ? t.header.dashboard : t.howItWorks.cta}
</Link>
<Link
href={githubUrl}

View file

@ -3,10 +3,12 @@
import Link from "next/link";
import { MulticaIcon } from "@/components/multica-icon";
import { cn } from "@/lib/utils";
import { useAuthStore } from "@/features/auth";
import { useLocale, locales, localeLabels } from "../i18n";
export function LandingFooter() {
const { t, locale, setLocale } = useLocale();
const user = useAuthStore((s) => s.user);
const groups = Object.values(t.footer.groups);
return (
@ -27,10 +29,10 @@ export function LandingFooter() {
</p>
<div className="mt-6">
<Link
href="/login"
href={user ? "/issues" : "/login"}
className="inline-flex items-center justify-center rounded-[11px] bg-white px-5 py-2.5 text-[13px] font-semibold text-[#0a0d12] transition-colors hover:bg-white/88"
>
{t.footer.cta}
{user ? t.header.dashboard : t.footer.cta}
</Link>
</div>
</div>

View file

@ -3,6 +3,7 @@
import Link from "next/link";
import { MulticaIcon } from "@/components/multica-icon";
import { cn } from "@/lib/utils";
import { useAuthStore } from "@/features/auth";
import { useLocale } from "../i18n";
import { GitHubMark, githubUrl, headerButtonClassName } from "./shared";
@ -12,6 +13,7 @@ export function LandingHeader({
variant?: "dark" | "light";
}) {
const { t } = useLocale();
const user = useAuthStore((s) => s.user);
return (
<header
@ -52,10 +54,10 @@ export function LandingHeader({
{t.header.github}
</Link>
<Link
href="/login"
href={user ? "/issues" : "/login"}
className={headerButtonClassName("solid", variant)}
>
{t.header.login}
{user ? t.header.dashboard : t.header.login}
</Link>
</div>
</div>

View file

@ -2,6 +2,7 @@
import Image from "next/image";
import Link from "next/link";
import { useAuthStore } from "@/features/auth";
import { useLocale } from "../i18n";
import {
ClaudeCodeLogo,
@ -13,6 +14,7 @@ import {
export function LandingHero() {
const { t } = useLocale();
const user = useAuthStore((s) => s.user);
return (
<div className="relative min-h-full overflow-hidden bg-[#05070b] text-white">
@ -35,8 +37,8 @@ export function LandingHero() {
</p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
<Link href="/login" className={heroButtonClassName("solid")}>
{t.hero.cta}
<Link href={user ? "/issues" : "/login"} className={heroButtonClassName("solid")}>
{user ? t.header.dashboard : t.hero.cta}
</Link>
<Link
href={githubUrl}
@ -93,11 +95,14 @@ function ProductImage({ alt }: { alt: string }) {
return (
<div>
<div className="relative overflow-hidden border border-white/14">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
<Image
src="/images/landing-hero.png"
alt={alt}
className="block w-full"
width={3532}
height={2382}
className="block h-auto w-full"
sizes="(max-width: 1320px) 100vw, 1320px"
quality={85}
/>
</div>
</div>

View file

@ -7,17 +7,8 @@ import type { LandingDict, Locale } from "./types";
const dictionaries: Record<Locale, LandingDict> = { en, zh };
const STORAGE_KEY = "multica-locale";
function getInitialLocale(): Locale {
if (typeof window === "undefined") return "en";
const stored = localStorage.getItem(STORAGE_KEY);
if (stored === "en" || stored === "zh") return stored;
// Detect from browser language
const lang = navigator.language;
if (lang.startsWith("zh")) return "zh";
return "en";
}
const COOKIE_NAME = "multica-locale";
const COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 1 year
type LocaleContextValue = {
locale: Locale;
@ -27,12 +18,18 @@ type LocaleContextValue = {
const LocaleContext = createContext<LocaleContextValue | null>(null);
export function LocaleProvider({ children }: { children: React.ReactNode }) {
const [locale, setLocaleState] = useState<Locale>(getInitialLocale);
export function LocaleProvider({
children,
initialLocale = "en",
}: {
children: React.ReactNode;
initialLocale?: Locale;
}) {
const [locale, setLocaleState] = useState<Locale>(initialLocale);
const setLocale = useCallback((l: Locale) => {
setLocaleState(l);
localStorage.setItem(STORAGE_KEY, l);
document.cookie = `${COOKIE_NAME}=${l}; path=/; max-age=${COOKIE_MAX_AGE}; SameSite=Lax`;
}, []);
return (

View file

@ -5,6 +5,7 @@ export const en: LandingDict = {
header: {
github: "GitHub",
login: "Log in",
dashboard: "Dashboard",
},
hero: {

View file

@ -20,7 +20,7 @@ type FooterGroup = {
};
export type LandingDict = {
header: { github: string; login: string };
header: { github: string; login: string; dashboard: string };
hero: {
headlineLine1: string;
headlineLine2: string;

View file

@ -5,6 +5,7 @@ export const zh: LandingDict = {
header: {
github: "GitHub",
login: "\u767b\u5f55",
dashboard: "\u8fdb\u5165\u5de5\u4f5c\u53f0",
},
hero: {

View file

@ -8,6 +8,10 @@ config({ path: resolve(__dirname, "../../.env") });
const remoteApiUrl = process.env.REMOTE_API_URL || "http://localhost:8080";
const nextConfig: NextConfig = {
images: {
formats: ["image/avif", "image/webp"],
qualities: [75, 80, 85],
},
async rewrites() {
return [
{

View file

@ -74,8 +74,8 @@ export class ApiClient {
localStorage.removeItem("multica_workspace_id");
this.token = null;
this.workspaceId = null;
if (window.location.pathname !== "/login") {
window.location.href = "/login";
if (window.location.pathname !== "/") {
window.location.href = "/";
}
}
}