From 3bd504fe5f5ddb076546e12b827e41b17d28f3bd Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:54:11 +0800 Subject: [PATCH] feat(web): SEO optimization and auth flow improvements for landing pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 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) --- apps/web/app/(auth)/login/page.tsx | 9 +++ .../(dashboard)/_components/app-sidebar.tsx | 2 +- apps/web/app/(dashboard)/layout.tsx | 2 +- apps/web/app/(landing)/about/page.tsx | 75 +++++-------------- apps/web/app/(landing)/changelog/page.tsx | 67 ++++------------- apps/web/app/(landing)/homepage/page.tsx | 21 ++++++ apps/web/app/(landing)/layout.tsx | 57 +++++++++++++- apps/web/app/(landing)/page.tsx | 44 +++++------ apps/web/app/layout.tsx | 37 +++++++-- apps/web/app/robots.ts | 28 +++++++ apps/web/app/sitemap.ts | 26 +++++++ .../landing/components/about-page-client.tsx | 62 +++++++++++++++ .../components/changelog-page-client.tsx | 55 ++++++++++++++ .../landing/components/features-section.tsx | 13 +++- .../components/how-it-works-section.tsx | 6 +- .../landing/components/landing-footer.tsx | 6 +- .../landing/components/landing-header.tsx | 6 +- .../landing/components/landing-hero.tsx | 15 ++-- apps/web/features/landing/i18n/context.tsx | 25 +++---- apps/web/features/landing/i18n/en.ts | 1 + apps/web/features/landing/i18n/types.ts | 2 +- apps/web/features/landing/i18n/zh.ts | 1 + apps/web/next.config.ts | 4 + apps/web/shared/api/client.ts | 4 +- 24 files changed, 392 insertions(+), 176 deletions(-) create mode 100644 apps/web/app/(landing)/homepage/page.tsx create mode 100644 apps/web/app/robots.ts create mode 100644 apps/web/app/sitemap.ts create mode 100644 apps/web/features/landing/components/about-page-client.tsx create mode 100644 apps/web/features/landing/components/changelog-page-client.tsx diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx index 215a34ae..a7ce0b7a 100644 --- a/apps/web/app/(auth)/login/page.tsx +++ b/apps/web/app/(auth)/login/page.tsx @@ -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(""); diff --git a/apps/web/app/(dashboard)/_components/app-sidebar.tsx b/apps/web/app/(dashboard)/_components/app-sidebar.tsx index 42a66298..7657d039 100644 --- a/apps/web/app/(dashboard)/_components/app-sidebar.tsx +++ b/apps/web/app/(dashboard)/_components/app-sidebar.tsx @@ -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 ( diff --git a/apps/web/app/(dashboard)/layout.tsx b/apps/web/app/(dashboard)/layout.tsx index e9e78d16..6694f756 100644 --- a/apps/web/app/(dashboard)/layout.tsx +++ b/apps/web/app/(dashboard)/layout.tsx @@ -22,7 +22,7 @@ export default function DashboardLayout({ useEffect(() => { if (!isLoading && !user) { - router.push("/login"); + router.push("/"); } }, [user, isLoading, router]); diff --git a/apps/web/app/(landing)/about/page.tsx b/apps/web/app/(landing)/about/page.tsx index 5d842239..6961aaaf 100644 --- a/apps/web/app/(landing)/about/page.tsx +++ b/apps/web/app/(landing)/about/page.tsx @@ -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 ( - <> - -
-
-

- {t.about.title} -

-
-

- {n.prefix} - - {n.mul} - - {n.tiplexed} - - {n.i} - - {n.nformationAnd} - - {n.c} - - {n.omputing} - - {n.a} - - {n.gent} -

- {t.about.paragraphs.map((p, i) => ( -

{p}

- ))} -
- -
- - - {t.about.cta} - -
-
-
- - - ); + return ; } diff --git a/apps/web/app/(landing)/changelog/page.tsx b/apps/web/app/(landing)/changelog/page.tsx index e1351687..ac28db21 100644 --- a/apps/web/app/(landing)/changelog/page.tsx +++ b/apps/web/app/(landing)/changelog/page.tsx @@ -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 ( - <> - -
-
-

- {t.changelog.title} -

-

- {t.changelog.subtitle} -

- -
- {t.changelog.entries.map((release) => ( -
-
- - v{release.version} - - - {release.date} - -
-

- {release.title} -

-
    - {release.changes.map((change) => ( -
  • - - {change} -
  • - ))} -
-
- ))} -
-
-
- - - ); + return ; } diff --git a/apps/web/app/(landing)/homepage/page.tsx b/apps/web/app/(landing)/homepage/page.tsx new file mode 100644 index 00000000..805bc4b2 --- /dev/null +++ b/apps/web/app/(landing)/homepage/page.tsx @@ -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 ; +} diff --git a/apps/web/app/(landing)/layout.tsx b/apps/web/app/(landing)/layout.tsx index 035f0e45..8ed18845 100644 --- a/apps/web/app/(landing)/layout.tsx +++ b/apps/web/app/(landing)/layout.tsx @@ -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 { + // 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 ( -
- {children} -
+ <> +