Improve landing performance and analytics funnel

This commit is contained in:
2569718930@qq.com
2026-05-30 19:50:50 +08:00
parent 3fcda3f3cd
commit 9bcfd9d1eb
15 changed files with 336 additions and 95 deletions
@@ -21,6 +21,7 @@ import {
getSupabaseBrowserClient,
hasSupabasePublicEnv,
} from "@/lib/supabase/client";
import { markAnalyticsOnce, trackAppEvent } from "@/lib/app-analytics";
const COVERAGE_EN = [
"Live airport observations",
@@ -76,6 +77,12 @@ function InstitutionalLandingScreen() {
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);
@@ -90,6 +97,26 @@ function InstitutionalLandingScreen() {
const coverage = isEn ? COVERAGE_EN : COVERAGE_ZH;
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
? [
{
@@ -176,6 +203,7 @@ function InstitutionalLandingScreen() {
<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" : "进入"}
@@ -193,12 +221,14 @@ function InstitutionalLandingScreen() {
<>
<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" : "开始使用"}
@@ -226,6 +256,11 @@ function InstitutionalLandingScreen() {
<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" : "进入产品"}
@@ -252,11 +287,19 @@ function InstitutionalLandingScreen() {
<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>
</div>
<img
src="/static/web.png"
alt={isEn ? "PolyWeather terminal preview" : "PolyWeather 终端预览"}
className="mt-2 aspect-[16/9] w-full rounded-md border border-slate-100 object-cover object-top"
/>
<div className="mt-2 aspect-[16/9] overflow-hidden rounded-md border border-slate-100 bg-slate-100">
<img
src="/static/web.webp"
width="680"
height="340"
alt={isEn ? "PolyWeather terminal preview" : "PolyWeather 终端预览"}
className="h-full w-full object-cover object-top"
decoding="async"
fetchPriority="high"
loading="eager"
sizes="(min-width: 1024px) 960px, calc(100vw - 48px)"
/>
</div>
</div>
<div className="mx-auto mt-8 grid max-w-5xl gap-2 sm:grid-cols-2 lg:grid-cols-4">
@@ -6,18 +6,40 @@ function assert(condition: unknown, message: string) {
}
export function runTests() {
const root = projectRoot();
const source = fs.readFileSync(
path.join(projectRoot(), "components", "landing", "InstitutionalLandingPage.tsx"),
path.join(root, "components", "landing", "InstitutionalLandingPage.tsx"),
"utf8",
);
const appPageSource = fs.readFileSync(path.join(projectRoot(), "app", "page.tsx"), "utf8");
const appPageSource = fs.readFileSync(path.join(root, "app", "page.tsx"), "utf8");
const pngPath = path.join(root, "public", "static", "web.png");
const webpPath = path.join(root, "public", "static", "web.webp");
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(source.includes("/static/web.png"), "landing page must show the product preview image");
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,
"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");
assert(!source.includes('src="/static/web.png"'), "landing hero must not use the heavy PNG as its primary LCP image");
assert(
source.includes('width="680"') &&
source.includes('height="340"') &&
source.includes('fetchPriority="high"') &&
source.includes('decoding="async"'),
"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",
);
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");
assert(source.includes("20 USDC") && source.includes("+3500 积分"), "landing page must describe referral discount and reward");