From 9915e2759fcd42b25473dadcbb83c65a5b2fcf13 Mon Sep 17 00:00:00 2001 From: willxue Date: Mon, 7 Sep 2026 21:32:58 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20landing=20hero=20rebuild=20?= =?UTF-8?q?=E2=80=94=20demonstrate=20the=20product,=20honest=20positioning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: the landing page described the product (82 text nodes, zero product imagery) instead of demonstrating it. The hero CTA 立即开始 led straight into the invite-code wall, open-source/self-host positioning was absent from the first screen, and the page presented config defaults (a 20/day send limit) as key metrics alongside fabricated testimonials — trust risks for an open-source project. Constraints / tradeoffs: - Hero becomes a two-column composition: copy (OSS pills 开源/自托管/ Cloudflare 边缘, serif headline with the rotating word now in brand orange, honest CTAs 申请邀请 + 自托管部署 → hosted docs) beside an auto-playing inbox demo card (mail arrives → code chip extracts → copy state), replacing the abstract 634px orb. - The stats marquee (config defaults) is replaced by an extraction playground: a textarea where pasted text is processed by the REAL buildMessageExtraction from @wemail/shared client-side — the same pure function the Worker runs. Verified live: re-extraction on input, Chinese codes, price/date rejection. - Demo steps are JS-timed with a reducedMotion prop that skips to the final frame; a blanket prefers-reduced-motion CSS guard now covers every landing animation (previously 1 guard in 2430 lines). - Metrics now show verifiable facts (500+ tests, 5-min self-host, 300+ edge locations, 100% open source); testimonials relabeled 场景示例 with workflow scenarios, no invented personas. - Hero headline scales from 10rem editorial display to a column-fitting clamp so copy + demo + playground land above the fold. Verification: - Full matrix: lint 0, typecheck 0, 532 tests (30+169+333), Playwright E2E at baseline parity (landing smoke green). - Runtime browser verification at 1440 and 390: two-column layout renders, playground live-extracts typed input (991177 extracted, 39.90 rejected), no horizontal overflow, demo steps advance, orb and marquee confirmed removed. - Landing test updated to the new CTA contract and extended with playground/demo-region assertions. Risks: hero visual identity shifts from centered editorial to split layout; the serif display type and grid identity are retained. Rejected alternatives: keeping the marquee with different numbers (config values read as limits, not achievements); linking the secondary CTA to the in-app /docs placeholder (it is a stub — hosted docs used instead). --- CHANGELOG.md | 6 + .../web/src/features/landing/HeroShowcase.tsx | 179 ++++++++ .../features/landing/WemailLandingPage.tsx | 104 +++-- .../src/features/landing/landing-content.ts | 37 +- apps/web/src/features/landing/landing.css | 411 +++++++++++++++--- apps/web/src/test/app.test.tsx | 7 +- hero-new-1440.png | Bin 0 -> 699426 bytes landing-full-1440.png | Bin 0 -> 10648776 bytes landing-hero-1440.png | Bin 0 -> 1204074 bytes landing-mid-1440.png | Bin 0 -> 933053 bytes 10 files changed, 620 insertions(+), 124 deletions(-) create mode 100644 apps/web/src/features/landing/HeroShowcase.tsx create mode 100644 hero-new-1440.png create mode 100644 landing-full-1440.png create mode 100644 landing-hero-1440.png create mode 100644 landing-mid-1440.png diff --git a/CHANGELOG.md b/CHANGELOG.md index ddb78d8..81db4ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Rebuilt the landing hero around demonstrating the product instead of describing it: an auto-playing inbox demo (mail arrives → verification code extracted → copied), an interactive extraction playground running the real Worker-side extraction engine client-side on pasted text, open-source/self-host/edge positioning pills, honest CTAs (申请邀请 + 自托管部署 instead of 立即开始 hitting the invite wall), and the rotating headline word now carries the brand accent. +- Landing content honesty: key metrics are now verifiable facts (500+ automated tests, 5-minute self-host deploy, 300+ Cloudflare edge locations, 100% open source) instead of config defaults, and the testimonials section is labeled 场景示例 with workflow scenarios instead of fabricated personas. +- Added a blanket prefers-reduced-motion guard covering all landing-page animations. + ## [0.4.0] - 2026-09-06 ### Added diff --git a/apps/web/src/features/landing/HeroShowcase.tsx b/apps/web/src/features/landing/HeroShowcase.tsx new file mode 100644 index 0000000..91a57be --- /dev/null +++ b/apps/web/src/features/landing/HeroShowcase.tsx @@ -0,0 +1,179 @@ +import { useEffect, useMemo, useState } from "react"; +import { Check, Copy, Inbox, KeyRound, Link2, Timer } from "lucide-react"; + +import { buildMessageExtraction, type ExtractionResult } from "@wemail/shared"; + +// --------------------------------------------------------------------------- +// Hero showcase: the landing page's job is to demonstrate the product, not +// describe it. The demo card auto-plays the core loop (mail arrives → code +// extracted → copied); the playground lets visitors paste any email text and +// watch the real extraction engine (the same pure function the Worker runs) +// work client-side. +// --------------------------------------------------------------------------- + +const DEMO_STEP_DURATIONS = [700, 1000, 1200, 1500, 1700]; +const DEMO_FINAL_STEP = DEMO_STEP_DURATIONS.length - 1; + +function useDemoStep(reducedMotion: boolean) { + const [step, setStep] = useState(0); + + useEffect(() => { + if (reducedMotion) { + setStep(DEMO_FINAL_STEP); + return; + } + + const advance = (current: number) => { + const next = current >= DEMO_FINAL_STEP ? 0 : current + 1; + setStep(next); + window.setTimeout(() => advance(next), DEMO_STEP_DURATIONS[next]); + }; + + const timer = window.setTimeout(() => advance(0), DEMO_STEP_DURATIONS[0]); + return () => window.clearTimeout(timer); + }, [reducedMotion]); + + return reducedMotion ? DEMO_FINAL_STEP : step; +} + +export function HeroDemoCard({ reducedMotion }: { reducedMotion: boolean }) { + const step = useDemoStep(reducedMotion); + + return ( +
+
+ + ops@wemail.dev + + + +
+ +
+ {step === 0 ? ( +

等待新邮件…

+ ) : ( +
+
+ GitHub + 刚刚 +
+

[WeMail] Please verify your device

+

Your verification code is 482914. It expires in 10 minutes.

+ + {step >= 2 ? ( +
+ + + + +
+ ) : null} +
+ )} +
+ +
+ + +
+
+ ); +} + +const PLAYGROUND_SAMPLE = [ + "您的验证码为 836-592,10 分钟内有效。", + "点击确认注册:https://app.example.com/verify?token=abc123", + "不希望再收到?退订:https://example.com/unsubscribe" +].join("\n"); + +const chipKindByType: Record = { + auth_code: "code", + auth_link: "link", + service_link: "link", + subscription_link: "link", + other_link: "link" +}; + +// The shared engine labels are English ("Verification code"); the landing is +// Chinese-first, so chips display a localized label. +const chipLabelByType: Record = { + auth_code: "验证码", + auth_link: "验证链接", + service_link: "服务链接", + subscription_link: "退订链接", + other_link: "有用链接" +}; + +function PlaygroundChip({ item }: { item: ExtractionResult }) { + const kind = chipKindByType[item.type] ?? "link"; + const isCode = kind === "code"; + const displayValue = isCode ? item.value : item.value.replace(/^https?:\/\//, ""); + + return ( + + {isCode ? ( + + ); +} + +export function HeroPlayground() { + const [text, setText] = useState(PLAYGROUND_SAMPLE); + const envelope = useMemo(() => buildMessageExtraction({ subject: "示例邮件", text }), [text]); + + return ( +
+
+

试一试

+

粘贴任意邮件文本,看提取引擎工作

+

+ 这就是 Worker 收信时运行的同一个提取函数——验证码、链接分类、有效期,全部在你的浏览器里实时计算。 +

+
+
+ +