Improve landing performance and analytics funnel
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user