Optimize landing server rendering

This commit is contained in:
2569718930@qq.com
2026-05-30 23:43:07 +08:00
parent 30e9513ce2
commit bf0fbe1b1d
9 changed files with 526 additions and 216 deletions
-2
View File
@@ -1,6 +1,5 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { PreloadTerminalData } from "@/components/landing/PreloadTerminalData";
import { InstitutionalLandingPage } from "@/components/landing/InstitutionalLandingPage";
export const metadata: Metadata = {
@@ -69,7 +68,6 @@ export default async function HomePage({
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<PreloadTerminalData />
<InstitutionalLandingPage />
</>
);
@@ -1,27 +1,15 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { cookies, headers } from "next/headers";
import { LandingAnalytics } from "@/components/landing/LandingAnalytics";
import {
ArrowRight,
Bell,
Check,
CheckCircle2,
Clock3,
CloudSun,
Database,
Gauge,
LineChart,
Radar,
ShieldCheck,
Users,
} from "lucide-react";
import { I18nProvider, useI18n } from "@/hooks/useI18n";
LandingHeaderActions,
LandingHeroActions,
} from "@/components/landing/LandingAuthActions";
import {
getSupabaseBrowserClient,
hasSupabasePublicEnv,
} from "@/lib/supabase/client";
import { markAnalyticsOnce, trackAppEvent } from "@/lib/app-analytics";
LANDING_LOCALE_COOKIE,
pickLandingLocale,
type LandingLocale,
} from "@/components/landing/landingLocale";
const COVERAGE_EN = [
"Live airport observations",
@@ -59,6 +47,123 @@ const PRO_FEATURES_ZH = [
"订阅与准入问题优先支持",
];
type IconName =
| "radar"
| "gauge"
| "shield"
| "cloudSun"
| "lineChart"
| "bell"
| "clock"
| "database"
| "check"
| "arrow";
function LandingIcon({
className,
name,
size = 16,
}: {
className?: string;
name: IconName;
size?: number;
}) {
const common = {
"aria-hidden": true,
className,
fill: "none",
height: size,
stroke: "currentColor",
strokeLinecap: "round" as const,
strokeLinejoin: "round" as const,
strokeWidth: 2,
viewBox: "0 0 24 24",
width: size,
};
switch (name) {
case "radar":
return (
<svg {...common}>
<path d="M12 3a9 9 0 1 0 9 9" />
<path d="M12 7a5 5 0 1 0 5 5" />
<path d="M12 12l7-7" />
<path d="M16 4h4v4" />
</svg>
);
case "gauge":
return (
<svg {...common}>
<path d="M4 14a8 8 0 1 1 16 0" />
<path d="M12 14l4-4" />
<path d="M8 18h8" />
</svg>
);
case "shield":
return (
<svg {...common}>
<path d="M12 3l7 3v5c0 4.3-2.8 8.1-7 9-4.2-.9-7-4.7-7-9V6l7-3Z" />
<path d="M9 12l2 2 4-5" />
</svg>
);
case "cloudSun":
return (
<svg {...common}>
<path d="M8 14.5a4 4 0 0 1 7.8-1.2A3.3 3.3 0 1 1 17 20H8.5a2.8 2.8 0 0 1-.5-5.5Z" />
<path d="M16 3v2" />
<path d="M20.2 4.8l-1.4 1.4" />
<path d="M21 10h-2" />
<path d="M12 4.8l1.4 1.4" />
</svg>
);
case "lineChart":
return (
<svg {...common}>
<path d="M4 19V5" />
<path d="M4 19h16" />
<path d="M7 15l3-4 3 2 4-6" />
</svg>
);
case "bell":
return (
<svg {...common}>
<path d="M6 9a6 6 0 0 1 12 0c0 6 2 6 2 8H4c0-2 2-2 2-8Z" />
<path d="M10 20a2 2 0 0 0 4 0" />
</svg>
);
case "clock":
return (
<svg {...common}>
<circle cx="12" cy="12" r="8.5" />
<path d="M12 7v5l3 2" />
</svg>
);
case "database":
return (
<svg {...common}>
<ellipse cx="12" cy="6" rx="7" ry="3" />
<path d="M5 6v6c0 1.7 3.1 3 7 3s7-1.3 7-3V6" />
<path d="M5 12v6c0 1.7 3.1 3 7 3s7-1.3 7-3v-6" />
</svg>
);
case "check":
return (
<svg {...common}>
<path d="M5 12.5l4 4L19 7" />
</svg>
);
case "arrow":
return (
<svg {...common}>
<path d="M5 12h14" />
<path d="M13 6l6 6-6 6" />
</svg>
);
default:
return null;
}
}
function WeatherWorkflowIllustration() {
return (
<div
@@ -71,83 +176,55 @@ function WeatherWorkflowIllustration() {
);
}
function InstitutionalLandingScreen() {
const { locale, toggleLocale } = useI18n();
async function resolveLandingLocale(): Promise<LandingLocale> {
const [cookieStore, headerStore] = await Promise.all([cookies(), headers()]);
return pickLandingLocale(
cookieStore.get(LANDING_LOCALE_COOKIE)?.value,
headerStore.get("accept-language"),
);
}
function InstitutionalLandingScreen({ locale }: { locale: LandingLocale }) {
const isEn = locale === "en-US";
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [authChecked, setAuthChecked] = useState(false);
useEffect(() => {
if (markAnalyticsOnce("landing_view", "session")) {
trackAppEvent("landing_view", { entry: "landing" });
}
}, []);
useEffect(() => {
if (!hasSupabasePublicEnv()) {
setAuthChecked(true);
return;
}
const supabase = getSupabaseBrowserClient();
supabase.auth.getSession().then(({ data: { session } }) => {
setIsAuthenticated(!!session?.user);
setAuthChecked(true);
});
}, []);
const coverage = isEn ? COVERAGE_EN : COVERAGE_ZH;
const coverageAccentClasses = [
"bg-sky-100 text-sky-700",
"bg-emerald-100 text-emerald-700",
"bg-amber-100 text-amber-700",
];
const coverageIcons: IconName[] = ["cloudSun", "lineChart", "bell"];
const trackLoginStart = (mode: "login" | "signup") => {
trackAppEvent("login_start", {
entry: "landing",
mode,
next: "/terminal",
});
};
const trackEnterTerminal = (entry: string) => {
trackAppEvent("enter_terminal", {
entry,
authenticated: isAuthenticated,
});
};
const trackTerminalAuthStart = (entry: string, mode: "login" | "signup") => {
trackEnterTerminal(entry);
trackLoginStart(mode);
};
const platformCards = isEn
const platformCards: Array<{ body: string; icon: IconName; title: string }> = isEn
? [
{
icon: Radar,
icon: "radar",
title: "Live Evidence",
body: "Airport observations, model spreads, and deviation checks stay in one calm workspace.",
},
{
icon: Gauge,
icon: "gauge",
title: "Daily Review",
body: "Scan the city board, compare forecasts, and keep the current decision context visible.",
},
{
icon: ShieldCheck,
icon: "shield",
title: "Access Control",
body: "Trial users get the same product experience as Pro, except the paid Telegram group link stays hidden.",
},
]
: [
{
icon: Radar,
icon: "radar",
title: "实况证据",
body: "机场观测、模型分歧与偏差校验放在一个安静清晰的工作台里。",
},
{
icon: Gauge,
icon: "gauge",
title: "每日复盘",
body: "快速扫描城市面板、比较预报路径,并保留当前判断上下文。",
},
{
icon: ShieldCheck,
icon: "shield",
title: "权益分层",
body: "试用期权益和 Pro 一致,唯一例外是不显示付费 Telegram 群链接。",
},
@@ -169,6 +246,7 @@ function InstitutionalLandingScreen() {
return (
<div className="min-h-screen bg-[#fbfbfa] text-slate-950 antialiased">
<LandingAnalytics />
<header className="sticky top-0 z-30 border-b border-slate-200 bg-[#fbfbfa]/90 backdrop-blur-xl">
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between px-4 sm:px-6">
<Link href="/" className="flex items-center gap-2 transition-opacity hover:opacity-80">
@@ -187,56 +265,7 @@ function InstitutionalLandingScreen() {
</a>
</nav>
<div className="flex items-center gap-2">
<button
type="button"
onClick={toggleLocale}
className="inline-flex h-9 cursor-pointer items-center gap-1 rounded-md border border-slate-200 bg-white px-1 text-xs font-semibold text-slate-500 shadow-sm hover:border-slate-300"
>
<span className={`rounded px-2 py-1 ${!isEn ? "bg-slate-900 text-white" : ""}`}></span>
<span className={`rounded px-2 py-1 ${isEn ? "bg-slate-900 text-white" : ""}`}>EN</span>
</button>
{!authChecked ? (
<div className="h-9 w-24 animate-pulse rounded-md bg-slate-200" />
) : isAuthenticated ? (
<div className="flex items-center gap-2">
<Link
href="/terminal"
onClick={() => trackEnterTerminal("landing_header")}
className="inline-flex h-9 items-center gap-2 rounded-md bg-slate-950 px-3 text-sm font-semibold text-white hover:bg-slate-800"
>
{isEn ? "Open" : "进入"}
<ArrowRight size={14} />
</Link>
<Link
href="/account"
className="grid h-9 w-9 place-items-center rounded-md border border-slate-200 bg-white text-slate-600 shadow-sm hover:border-slate-300 hover:text-slate-950"
title={isEn ? "Account" : "账户"}
>
<Users size={15} />
</Link>
</div>
) : (
<>
<Link
href="/auth/login?next=%2Fterminal"
onClick={() => trackTerminalAuthStart("landing_header_login", "login")}
className="hidden h-9 items-center rounded-md px-3 text-sm font-semibold text-slate-600 hover:text-slate-950 sm:inline-flex"
>
{isEn ? "Log in" : "登录"}
</Link>
<Link
href="/auth/login?next=%2Fterminal&mode=signup"
onClick={() => trackTerminalAuthStart("landing_header_signup", "signup")}
className="inline-flex h-9 items-center gap-2 rounded-md bg-slate-950 px-3 text-sm font-semibold text-white hover:bg-slate-800"
>
{isEn ? "Start" : "开始使用"}
<ArrowRight size={14} />
</Link>
</>
)}
</div>
<LandingHeaderActions locale={locale} />
</div>
</header>
@@ -253,26 +282,7 @@ function InstitutionalLandingScreen() {
? "A calmer way to read airport weather, model forecasts, and intraday risk before the market moves."
: "用更轻松的方式阅读机场天气、模型预报和日内风险,在市场变化前完成判断。"}
</p>
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Link
href={authChecked && isAuthenticated ? "/terminal" : "/auth/login?next=%2Fterminal"}
onClick={() =>
authChecked && isAuthenticated
? trackEnterTerminal("landing_hero")
: trackTerminalAuthStart("landing_hero_login", "login")
}
className="inline-flex h-11 items-center justify-center gap-2 rounded-md bg-slate-950 px-5 text-sm font-bold text-white shadow-sm hover:bg-slate-800"
>
{isEn ? "Open product" : "进入产品"}
<ArrowRight size={15} />
</Link>
<Link
href="/account"
className="inline-flex h-11 items-center justify-center gap-2 rounded-md border border-slate-200 bg-white px-5 text-sm font-bold text-slate-700 shadow-sm hover:border-slate-300 hover:text-slate-950"
>
{isEn ? "Subscribe / account" : "订阅 / 账户"}
</Link>
</div>
<LandingHeroActions locale={locale} />
<p className="mt-4 text-sm text-slate-500">
{isEn
? "Start with a one-time 3-day trial. Trial access matches Pro except for the paid Telegram group link."
@@ -285,7 +295,9 @@ function InstitutionalLandingScreen() {
<span className="h-2.5 w-2.5 rounded-full bg-[#ff6b6b]" />
<span className="h-2.5 w-2.5 rounded-full bg-[#ffd166]" />
<span className="h-2.5 w-2.5 rounded-full bg-[#06d6a0]" />
<span className="ml-2 text-xs font-semibold text-slate-400">polyweather.app/terminal</span>
<span className="ml-2 text-xs font-semibold text-slate-400">
polyweather.app/terminal
</span>
</div>
<div className="mt-2 aspect-[16/9] overflow-hidden rounded-md border border-slate-100 bg-slate-100">
<img
@@ -304,8 +316,13 @@ function InstitutionalLandingScreen() {
<div className="mx-auto mt-8 grid max-w-5xl gap-2 sm:grid-cols-2 lg:grid-cols-4">
{heroStats.map((item) => (
<div key={item.label} className="rounded-lg border border-slate-200 bg-white px-4 py-4 shadow-sm">
<div className="font-mono text-lg font-black text-slate-950">{item.value}</div>
<div
key={item.label}
className="rounded-lg border border-slate-200 bg-white px-4 py-4 shadow-sm"
>
<div className="font-mono text-lg font-black text-slate-950">
{item.value}
</div>
<div className="mt-1 text-xs font-medium text-slate-500">{item.label}</div>
</div>
))}
@@ -320,15 +337,17 @@ function InstitutionalLandingScreen() {
{isEn ? "Platform" : "平台能力"}
</p>
<h2 className="mt-3 text-3xl font-black tracking-tight text-slate-950 sm:text-4xl">
{isEn ? "Like a tidy workspace for weather decisions." : "像整理好的工作区一样阅读天气决策。"}
{isEn
? "Like a tidy workspace for weather decisions."
: "像整理好的工作区一样阅读天气决策。"}
</h2>
</div>
<div className="mt-12 grid gap-4 md:grid-cols-3">
{platformCards.map(({ body, icon: Icon, title }) => (
{platformCards.map(({ body, icon, title }) => (
<article key={title} className="rounded-lg border border-slate-200 bg-[#fbfbfa] p-6 shadow-sm">
<div className="mb-5 inline-flex h-10 w-10 items-center justify-center rounded-md border border-slate-200 bg-white text-slate-800">
<Icon size={19} />
<LandingIcon name={icon} size={19} />
</div>
<h3 className="text-lg font-black text-slate-950">{title}</h3>
<p className="mt-3 text-sm leading-7 text-slate-600">{body}</p>
@@ -352,15 +371,16 @@ function InstitutionalLandingScreen() {
<div className="rounded-lg border border-slate-200 bg-white p-3 shadow-sm">
<div className="grid gap-2 sm:grid-cols-2">
{coverage.map((item, index) => (
<div key={item} className="flex items-center gap-3 rounded-md border border-slate-100 bg-[#fbfbfa] px-4 py-3">
<span className={`grid h-8 w-8 place-items-center rounded-md ${
index % 3 === 0
? "bg-sky-100 text-sky-700"
: index % 3 === 1
? "bg-emerald-100 text-emerald-700"
: "bg-amber-100 text-amber-700"
}`}>
{index % 3 === 0 ? <CloudSun size={16} /> : index % 3 === 1 ? <LineChart size={16} /> : <Bell size={16} />}
<div
key={item}
className="flex items-center gap-3 rounded-md border border-slate-100 bg-[#fbfbfa] px-4 py-3"
>
<span
className={`grid h-8 w-8 place-items-center rounded-md ${
coverageAccentClasses[index % coverageAccentClasses.length]
}`}
>
<LandingIcon name={coverageIcons[index % coverageIcons.length]} />
</span>
<span className="text-sm font-semibold text-slate-700">{item}</span>
</div>
@@ -377,7 +397,9 @@ function InstitutionalLandingScreen() {
{isEn ? "Pricing" : "定价"}
</p>
<h2 className="mt-3 text-3xl font-black tracking-tight text-slate-950 sm:text-4xl">
{isEn ? "Try first, upgrade when it becomes part of your workflow." : "先试用,确认进入工作流后再开通 Pro。"}
{isEn
? "Try first, upgrade when it becomes part of your workflow."
: "先试用,确认进入工作流后再开通 Pro。"}
</h2>
<p className="mt-4 text-base leading-8 text-slate-600">
{isEn
@@ -389,7 +411,7 @@ function InstitutionalLandingScreen() {
<div className="mt-12 grid gap-4 md:grid-cols-3">
<div className="flex flex-col rounded-lg border border-slate-200 bg-[#fbfbfa] p-6 shadow-sm">
<div className="flex items-center gap-2 text-sm font-bold text-emerald-700">
<Clock3 size={16} />
<LandingIcon name="clock" />
{isEn ? "Trial" : "试用"}
</div>
<h3 className="mt-5 text-2xl font-black text-slate-950">
@@ -405,7 +427,7 @@ function InstitutionalLandingScreen() {
className="mt-8 inline-flex h-11 items-center justify-center gap-2 rounded-md border border-slate-200 bg-white text-sm font-bold text-slate-700 shadow-sm hover:border-slate-300 hover:text-slate-950"
>
{isEn ? "Start trial" : "开始试用"}
<ArrowRight size={15} />
<LandingIcon name="arrow" size={15} />
</Link>
</div>
@@ -414,8 +436,8 @@ function InstitutionalLandingScreen() {
{isEn ? "Popular" : "常用"}
</div>
<div className="flex items-center gap-2 text-sm font-bold text-sky-700">
<Database size={16} />
{isEn ? "Pro" : "Pro"}
<LandingIcon name="database" />
Pro
</div>
<h3 className="mt-5 text-2xl font-black text-slate-950">
{isEn ? "Pro Monthly" : "Pro 月付"}
@@ -435,7 +457,11 @@ function InstitutionalLandingScreen() {
<ul className="mt-7 space-y-3 border-t border-slate-200 pt-6">
{(isEn ? PRO_FEATURES_EN : PRO_FEATURES_ZH).map((feature) => (
<li key={feature} className="flex items-start gap-3">
<Check size={15} className="mt-0.5 shrink-0 text-slate-500" />
<LandingIcon
name="check"
size={15}
className="mt-0.5 shrink-0 text-slate-500"
/>
<span className="text-sm leading-6 text-slate-700">{feature}</span>
</li>
))}
@@ -445,13 +471,13 @@ function InstitutionalLandingScreen() {
className="mt-8 inline-flex h-11 items-center justify-center gap-2 rounded-md bg-slate-950 text-sm font-bold text-white hover:bg-slate-800"
>
{isEn ? "Subscribe monthly" : "订阅月付 Pro"}
<ArrowRight size={15} />
<LandingIcon name="arrow" size={15} />
</Link>
</div>
<div className="flex flex-col rounded-lg border border-slate-200 bg-[#fbfbfa] p-6 shadow-sm">
<div className="flex items-center gap-2 text-sm font-bold text-amber-700">
<LineChart size={16} />
<LandingIcon name="lineChart" />
{isEn ? "Quarterly" : "季度"}
</div>
<h3 className="mt-5 text-2xl font-black text-slate-950">
@@ -476,7 +502,7 @@ function InstitutionalLandingScreen() {
className="mt-8 inline-flex h-11 items-center justify-center gap-2 rounded-md border border-slate-200 bg-white text-sm font-bold text-slate-700 shadow-sm hover:border-slate-300 hover:text-slate-950"
>
{isEn ? "Choose quarterly" : "选择季度 Pro"}
<ArrowRight size={15} />
<LandingIcon name="arrow" size={15} />
</Link>
</div>
</div>
@@ -487,10 +513,7 @@ function InstitutionalLandingScreen() {
);
}
export function InstitutionalLandingPage() {
return (
<I18nProvider>
<InstitutionalLandingScreen />
</I18nProvider>
);
export async function InstitutionalLandingPage() {
const locale = await resolveLandingLocale();
return <InstitutionalLandingScreen locale={locale} />;
}
@@ -0,0 +1,20 @@
"use client";
import { useEffect } from "react";
export function LandingAnalytics() {
useEffect(() => {
let active = true;
void import("@/lib/app-analytics").then(({ markAnalyticsOnce, trackAppEvent }) => {
if (!active) return;
if (markAnalyticsOnce("landing_view", "session")) {
trackAppEvent("landing_view", { entry: "landing" });
}
});
return () => {
active = false;
};
}, []);
return null;
}
@@ -0,0 +1,191 @@
"use client";
import Link from "next/link";
import { useEffect, useState } from "react";
import { LandingLocaleToggle } from "@/components/landing/LandingLocaleToggle";
import type { LandingLocale } from "@/components/landing/landingLocale";
let authStatePromise: Promise<boolean> | null = null;
function getLandingAuthState() {
if (!authStatePromise) {
authStatePromise = Promise.resolve()
.then(async () => {
const { getSupabaseBrowserClient, hasSupabasePublicEnv } = await import(
"@/lib/supabase/client"
);
if (!hasSupabasePublicEnv()) return false;
const supabase = getSupabaseBrowserClient();
const {
data: { session },
} = await supabase.auth.getSession();
return !!session?.user;
})
.catch(() => false);
}
return authStatePromise;
}
function useLandingAuthState() {
const [state, setState] = useState({ checked: false, authenticated: false });
useEffect(() => {
let active = true;
getLandingAuthState().then((authenticated) => {
if (!active) return;
setState({ checked: true, authenticated });
});
return () => {
active = false;
};
}, []);
return state;
}
function ArrowIcon() {
return (
<svg aria-hidden="true" viewBox="0 0 20 20" className="h-3.5 w-3.5" fill="none">
<path
d="M4 10h11m-4.5-4.5L15 10l-4.5 4.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
</svg>
);
}
function UserIcon() {
return (
<svg aria-hidden="true" viewBox="0 0 20 20" className="h-4 w-4" fill="none">
<path
d="M10 10a3.2 3.2 0 1 0 0-6.4A3.2 3.2 0 0 0 10 10Zm-5.5 6.4c.7-2.5 2.7-4 5.5-4s4.8 1.5 5.5 4"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
/>
</svg>
);
}
function trackLandingEvent(
eventType: "enter_terminal" | "login_start",
payload: Record<string, unknown>,
) {
void import("@/lib/app-analytics")
.then(({ trackAppEvent }) => {
trackAppEvent(eventType, payload);
})
.catch(() => {});
}
function trackLoginStart(mode: "login" | "signup") {
trackLandingEvent("login_start", {
entry: "landing",
mode,
next: "/terminal",
});
}
function trackEnterTerminal(entry: string, authenticated: boolean) {
trackLandingEvent("enter_terminal", {
entry,
authenticated,
});
}
function trackTerminalAuthStart(entry: string, mode: "login" | "signup") {
trackLandingEvent("enter_terminal", {
entry,
authenticated: false,
});
trackLoginStart(mode);
}
export function LandingHeaderActions({ locale }: { locale: LandingLocale }) {
const isEn = locale === "en-US";
const { authenticated, checked } = useLandingAuthState();
return (
<div className="flex items-center gap-2">
<LandingLocaleToggle locale={locale} />
{!checked ? (
<div className="h-9 w-24 animate-pulse rounded-md bg-slate-200" />
) : authenticated ? (
<div className="flex items-center gap-2">
<Link
href="/terminal"
onClick={() => trackEnterTerminal("landing_header", true)}
className="inline-flex h-9 items-center gap-2 rounded-md bg-slate-950 px-3 text-sm font-semibold text-white hover:bg-slate-800"
>
{isEn ? "Open" : "进入"}
<ArrowIcon />
</Link>
<Link
href="/account"
className="grid h-9 w-9 place-items-center rounded-md border border-slate-200 bg-white text-slate-600 shadow-sm hover:border-slate-300 hover:text-slate-950"
title={isEn ? "Account" : "账户"}
aria-label={isEn ? "Account" : "账户"}
>
<UserIcon />
</Link>
</div>
) : (
<>
<Link
href="/auth/login?next=%2Fterminal"
onClick={() => trackTerminalAuthStart("landing_header_login", "login")}
className="hidden h-9 items-center rounded-md px-3 text-sm font-semibold text-slate-600 hover:text-slate-950 sm:inline-flex"
>
{isEn ? "Log in" : "登录"}
</Link>
<Link
href="/auth/login?next=%2Fterminal&mode=signup"
onClick={() => trackTerminalAuthStart("landing_header_signup", "signup")}
className="inline-flex h-9 items-center gap-2 rounded-md bg-slate-950 px-3 text-sm font-semibold text-white hover:bg-slate-800"
>
{isEn ? "Start" : "开始使用"}
<ArrowIcon />
</Link>
</>
)}
</div>
);
}
export function LandingHeroActions({ locale }: { locale: LandingLocale }) {
const isEn = locale === "en-US";
const { authenticated, checked } = useLandingAuthState();
const href = checked && !authenticated ? "/auth/login?next=%2Fterminal" : "/terminal";
const handleOpenProduct = () => {
if (checked && !authenticated) {
trackTerminalAuthStart("landing_hero_login", "login");
return;
}
trackEnterTerminal("landing_hero", authenticated);
};
return (
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Link
href={href}
onClick={handleOpenProduct}
className="inline-flex h-11 items-center justify-center gap-2 rounded-md bg-slate-950 px-5 text-sm font-bold text-white shadow-sm hover:bg-slate-800"
>
{isEn ? "Open product" : "进入产品"}
<ArrowIcon />
</Link>
<Link
href="/account"
className="inline-flex h-11 items-center justify-center gap-2 rounded-md border border-slate-200 bg-white px-5 text-sm font-bold text-slate-700 shadow-sm hover:border-slate-300 hover:text-slate-950"
>
{isEn ? "Subscribe / account" : "订阅 / 账户"}
</Link>
</div>
);
}
@@ -0,0 +1,48 @@
"use client";
import { useTransition } from "react";
import { useRouter } from "next/navigation";
import {
LANDING_LOCALE_COOKIE,
nextLandingLocale,
type LandingLocale,
} from "@/components/landing/landingLocale";
const ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;
export function LandingLocaleToggle({ locale }: { locale: LandingLocale }) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const isEn = locale === "en-US";
const toggleLocale = () => {
const nextLocale = nextLandingLocale(locale);
document.cookie = `${LANDING_LOCALE_COOKIE}=${nextLocale}; Max-Age=${ONE_YEAR_SECONDS}; Path=/; SameSite=Lax`;
document.documentElement.lang = nextLocale;
try {
window.localStorage.setItem(LANDING_LOCALE_COOKIE, nextLocale);
} catch {
// Locale persistence is best-effort; the cookie is enough for SSR.
}
startTransition(() => {
router.refresh();
});
};
return (
<button
type="button"
onClick={toggleLocale}
disabled={isPending}
className="inline-flex h-9 cursor-pointer items-center gap-1 rounded-md border border-slate-200 bg-white px-1 text-xs font-semibold text-slate-500 shadow-sm hover:border-slate-300 disabled:cursor-wait disabled:opacity-70"
aria-label={isEn ? "Switch language" : "切换语言"}
>
<span className={`rounded px-2 py-1 ${!isEn ? "bg-slate-900 text-white" : ""}`}>
</span>
<span className={`rounded px-2 py-1 ${isEn ? "bg-slate-900 text-white" : ""}`}>
EN
</span>
</button>
);
}
@@ -1,37 +0,0 @@
"use client";
import { useEffect } from "react";
/**
* Warms the backend cache for public API endpoints while the user reads
* the landing page, so the first terminal load after login is faster.
*
* - /api/cities (public, triggers Python process warmup and connection pool init)
* - scan terminal (protected — skipped here; prefetched by the terminal
* component's stagger loading after login)
*/
export function PreloadTerminalData() {
useEffect(() => {
if (typeof window === "undefined" || typeof fetch !== "function") return;
const controller = new AbortController();
// Delay 2s so the landing page critical assets load first
const t = setTimeout(
() =>
fetch("/api/cities", {
cache: "no-store",
headers: { Accept: "application/json" },
signal: controller.signal,
}).catch(() => {}),
2000,
);
return () => {
controller.abort();
clearTimeout(t);
};
}, []);
return null;
}

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 237 KiB

@@ -11,18 +11,49 @@ export function runTests() {
path.join(root, "components", "landing", "InstitutionalLandingPage.tsx"),
"utf8",
);
const authActionsSource = fs.readFileSync(
path.join(root, "components", "landing", "LandingAuthActions.tsx"),
"utf8",
);
const analyticsSource = fs.readFileSync(
path.join(root, "components", "landing", "LandingAnalytics.tsx"),
"utf8",
);
const localeToggleSource = fs.readFileSync(
path.join(root, "components", "landing", "LandingLocaleToggle.tsx"),
"utf8",
);
const appPageSource = fs.readFileSync(path.join(root, "app", "page.tsx"), "utf8");
const pngPath = path.join(root, "public", "static", "web.png");
const publicPngPath = path.join(root, "public", "static", "web.png");
const fixturePngPath = path.join(root, "components", "landing", "__tests__", "fixtures", "web.png");
const webpPath = path.join(root, "public", "static", "web.webp");
assert(!source.startsWith('"use client"'), "landing body must be a Server Component");
assert(!source.includes("@/lib/supabase/client"), "landing body must not import the Supabase browser client");
assert(!source.includes("useEffect") && !source.includes("useState"), "landing body must not hydrate static content with React hooks");
assert(!source.includes("lucide-react"), "landing body must not import lucide-react for the LCP route");
assert(authActionsSource.startsWith('"use client"'), "auth actions must be isolated in a client island");
assert(analyticsSource.startsWith('"use client"'), "analytics must be isolated in a client island");
assert(localeToggleSource.startsWith('"use client"'), "locale toggle must be isolated in a client island");
assert(!authActionsSource.includes('from "@/lib/supabase/client"'), "auth island must not eagerly import the Supabase browser client");
assert(
authActionsSource.includes("await import(") && authActionsSource.includes('"@/lib/supabase/client"'),
"auth island must lazy-load the Supabase browser client after hydration",
);
assert(!analyticsSource.includes('from "@/lib/app-analytics"'), "analytics island must lazy-load analytics code");
assert(!authActionsSource.includes("lucide-react"), "auth island must avoid shipping lucide-react");
assert(!localeToggleSource.includes("lucide-react"), "locale island must avoid shipping lucide-react");
assert(!analyticsSource.includes("lucide-react"), "analytics island must avoid shipping lucide-react");
assert(source.includes("3 天免费试用"), "landing page must advertise the 3-day trial");
assert(source.includes("试用期权益和 Pro 一致,除了不显示付费 Telegram 群链接"), "landing page must state trial access matches Pro except the paid group link");
assert(!source.includes("高频刷新与 API 仍为 Pro 权益"), "landing page must not incorrectly exclude high-frequency refresh or API from trial access");
assert(source.includes("bg-[#fbfbfa]"), "landing page must use a light Notion-style background");
assert(source.includes("WeatherWorkflowIllustration"), "landing page must include a friendly illustration surface");
assert(!fs.existsSync(publicPngPath), "heavy PNG preview must not remain in public static assets");
assert(fs.existsSync(fixturePngPath), "PNG preview may only remain as a test fixture");
assert(fs.existsSync(webpPath), "landing page must ship a WebP preview image for the LCP product screenshot");
assert(
fs.statSync(webpPath).size < fs.statSync(pngPath).size * 0.65,
fs.statSync(webpPath).size < fs.statSync(fixturePngPath).size * 0.65,
"WebP preview must be materially smaller than the PNG LCP image",
);
assert(source.includes("/static/web.webp"), "landing page must load the lighter WebP product preview image");
@@ -35,10 +66,10 @@ export function runTests() {
"landing product preview must expose stable intrinsic dimensions and high fetch priority",
);
assert(
source.includes('trackAppEvent("landing_view"') &&
source.includes('trackAppEvent("login_start"') &&
source.includes('trackAppEvent("enter_terminal"'),
"landing page must emit the top-of-funnel analytics events",
analyticsSource.includes('"landing_view"') &&
authActionsSource.includes('"login_start"') &&
authActionsSource.includes('"enter_terminal"'),
"landing client islands must emit the top-of-funnel analytics events",
);
assert(source.includes("29.9") && source.includes("30 天"), "landing page must show monthly Pro pricing");
assert(source.includes("79.9") && source.includes("90 天"), "landing page must show quarterly Pro pricing");
@@ -49,6 +80,7 @@ export function runTests() {
assert(appPageSource.includes('price: "29.90"'), "JSON-LD must expose monthly Pro pricing");
assert(appPageSource.includes('price: "79.90"'), "JSON-LD must expose quarterly Pro pricing");
assert(!appPageSource.includes('price: "10.00"'), "legacy JSON-LD pricing must be removed");
assert(!appPageSource.includes("PreloadTerminalData"), "landing route must not add a fourth client island");
}
function projectRoot() {
@@ -0,0 +1,35 @@
export type LandingLocale = "zh-CN" | "en-US";
export const LANDING_LOCALE_COOKIE = "polyweather.locale";
export const DEFAULT_LANDING_LOCALE: LandingLocale = "zh-CN";
export function normalizeLandingLocale(value: string | null | undefined): LandingLocale | null {
if (!value) return null;
const normalized = value.trim().toLowerCase();
if (normalized === "zh" || normalized === "zh-cn" || normalized.startsWith("zh-")) {
return "zh-CN";
}
if (normalized === "en" || normalized === "en-us" || normalized.startsWith("en-")) {
return "en-US";
}
return null;
}
export function pickLandingLocale(
cookieLocale: string | null | undefined,
acceptLanguage: string | null | undefined,
): LandingLocale {
const fromCookie = normalizeLandingLocale(cookieLocale);
if (fromCookie) return fromCookie;
for (const part of String(acceptLanguage || "").split(",")) {
const locale = normalizeLandingLocale(part.split(";")[0]);
if (locale) return locale;
}
return DEFAULT_LANDING_LOCALE;
}
export function nextLandingLocale(locale: LandingLocale): LandingLocale {
return locale === "zh-CN" ? "en-US" : "zh-CN";
}