From 8fc26cc44fbaee10f17e1e14946c34ef07228819 Mon Sep 17 00:00:00 2001
From: Yaowei Zheng
Date: Thu, 6 Aug 2026 20:08:24 +0800
Subject: [PATCH] =?UTF-8?q?feat(landing):=20desktop-first=20homepage=20?=
=?UTF-8?q?=E2=80=94=20download=20CTA=20leads,=20CLI=20below=20the=20fold?=
=?UTF-8?q?=20(#221)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Claude Fable 5
---
packages/landing/src/lib/platform.ts | 15 ++++
packages/landing/src/lib/strings-en.ts | 14 ++-
packages/landing/src/lib/strings.ts | 17 ++--
packages/landing/src/pages/download.tsx | 12 +--
packages/landing/src/sections/cta.tsx | 20 +++--
packages/landing/src/sections/hero.tsx | 93 +++++++-------------
packages/landing/src/sections/quickstart.tsx | 5 +-
7 files changed, 86 insertions(+), 90 deletions(-)
create mode 100644 packages/landing/src/lib/platform.ts
diff --git a/packages/landing/src/lib/platform.ts b/packages/landing/src/lib/platform.ts
new file mode 100644
index 0000000..6eade95
--- /dev/null
+++ b/packages/landing/src/lib/platform.ts
@@ -0,0 +1,15 @@
+/**
+ * Coarse client-side OS detection, shared by the hero's platform-aware download
+ * button and the download page's platform cards. Detection only tunes emphasis
+ * (which label the hero button carries, which card gets badged) — absent or wrong
+ * detection never blocks a download.
+ */
+export type Platform = "mac" | "windows" | "linux";
+
+export function detectPlatform(): Platform | null {
+ const ua = navigator.userAgent;
+ if (/Windows/i.test(ua)) return "windows";
+ if (/Macintosh|Mac OS X/i.test(ua)) return "mac";
+ if (/Linux|X11/.test(ua) && !/Android/i.test(ua)) return "linux";
+ return null;
+}
diff --git a/packages/landing/src/lib/strings-en.ts b/packages/landing/src/lib/strings-en.ts
index a30fadd..53de34d 100644
--- a/packages/landing/src/lib/strings-en.ts
+++ b/packages/landing/src/lib/strings-en.ts
@@ -54,9 +54,14 @@ export const en: Strings = {
titleWords: ["Desktop", "Server"],
titleSuffix: "",
subtitle: "Create Self-Evolving Agents in One Click",
- ctaPrimary: "Get started",
+ /** Primary CTA: base label, and the platform-aware variant once the OS is detected. */
+ downloadCta: "Download the desktop app",
+ downloadCtaFor: (platform: string) => `Download for ${platform}`,
+ ctaQuickstart: "Get started",
ctaGithub: "GitHub",
- installHint: "One-line install (bundled Node runtime — unpack and run)",
+ /** Line under the CTAs: all installers on /download, CLI / self-hosted below the fold. */
+ downloadAll: "All platforms (macOS / Windows / Linux)",
+ cliAlt: "CLI and self-hosted install ↓",
stats: [
{ value: "1000+", label: "supported models" },
{ value: "1×CPU", label: "minimum footprint" },
@@ -80,7 +85,7 @@ export const en: Strings = {
},
offlineRelease: "Download offline packages from GitHub Releases",
desktopNote:
- "Prefer a standalone app? The desktop app packages the Web interface and the server into a terminal-free installer (macOS / Windows / Linux), sharing the same local data as a CLI install.",
+ "Already on the desktop app? A CLI install shares the same local data with it, and the two can be used side by side.",
desktopPage: "Go to the desktop download page",
},
@@ -485,7 +490,8 @@ export const en: Strings = {
title: "Complex AI development, made ever simpler",
subtitle:
"Through continuous evolution, PenguinHarness gives you a more efficient, more reliable, lower-hallucination and lower-cost Agent productivity engine.",
- install: "Install now",
+ download: "Download the desktop app",
+ quickstart: "Get started",
docs: "Read the docs",
},
diff --git a/packages/landing/src/lib/strings.ts b/packages/landing/src/lib/strings.ts
index 07fc4ce..786adb9 100644
--- a/packages/landing/src/lib/strings.ts
+++ b/packages/landing/src/lib/strings.ts
@@ -59,9 +59,14 @@ export const zh = {
titleWords: ["桌面", "服务器"],
titleSuffix: "上",
subtitle: "一键创建自进化 Agent",
- ctaPrimary: "快速开始",
+ /** Primary CTA: base label, and the platform-aware variant once the OS is detected. */
+ downloadCta: "下载桌面版",
+ downloadCtaFor: (platform: string) => `下载桌面版(${platform})`,
+ ctaQuickstart: "快速开始",
ctaGithub: "GitHub",
- installHint: "一行命令安装(内嵌 Node 运行时,解压即用)",
+ /** Line under the CTAs: all installers on /download, CLI / self-hosted below the fold. */
+ downloadAll: "全部平台下载(macOS / Windows / Linux)",
+ cliAlt: "命令行与自托管安装 ↓",
stats: [
{ value: "1000+", label: "支持模型数量" },
{ value: "1×CPU", label: "最低运行配置" },
@@ -70,7 +75,7 @@ export const zh = {
],
},
- /** Install-method switcher (hero + quick start): OS tabs, online/offline methods. */
+ /** Install-method switcher (quick start step 1): OS tabs, online/offline methods. */
install: {
linux: "Linux",
macos: "macOS",
@@ -85,8 +90,7 @@ export const zh = {
windows: "解压后也可直接双击 install.cmd 完成安装。",
},
offlineRelease: "前往 GitHub Releases 下载离线安装包",
- desktopNote:
- "更喜欢独立应用?桌面端把 Web 界面与服务端打包成免终端的安装程序(macOS / Windows / Linux),与 CLI 安装共用同一份本地数据。",
+ desktopNote: "已经装了桌面版?CLI 安装与桌面版共用同一份本地数据,两者可以随时并用。",
desktopPage: "前往桌面端下载页",
},
@@ -475,7 +479,8 @@ export const zh = {
title: "让复杂的 AI 开发越来越简单",
subtitle:
"通过不断进化,PenguinHarness 为你提供更高效、更可靠、更低幻觉、更低成本的 Agent 生产力引擎。",
- install: "立即安装",
+ download: "下载桌面版",
+ quickstart: "快速开始",
docs: "阅读文档",
},
diff --git a/packages/landing/src/pages/download.tsx b/packages/landing/src/pages/download.tsx
index 1ec67cd..f5a7da9 100644
--- a/packages/landing/src/pages/download.tsx
+++ b/packages/landing/src/pages/download.tsx
@@ -23,22 +23,14 @@ import {
OSS_ORIGIN,
RELEASES_URL,
} from "../lib/links";
+import { detectPlatform } from "../lib/platform";
+import type { Platform } from "../lib/platform";
import { Section } from "../components/section";
import { CodeCard } from "../components/code-card";
import { ChevronDownIcon, DownloadIcon, ExternalLinkIcon } from "../components/icons";
-type Platform = "mac" | "windows" | "linux";
const PLATFORMS: Platform[] = ["mac", "windows", "linux"];
-/** Coarse OS detection, used only to badge one card; absent or wrong detection changes nothing else. */
-function detectPlatform(): Platform | null {
- const ua = navigator.userAgent;
- if (/Windows/i.test(ua)) return "windows";
- if (/Macintosh|Mac OS X/i.test(ua)) return "mac";
- if (/Linux|X11/.test(ua) && !/Android/i.test(ua)) return "linux";
- return null;
-}
-
interface Mirror {
tag: string;
base: string;
diff --git a/packages/landing/src/sections/cta.tsx b/packages/landing/src/sections/cta.tsx
index f8ec440..2c86b5f 100644
--- a/packages/landing/src/sections/cta.tsx
+++ b/packages/landing/src/sections/cta.tsx
@@ -1,11 +1,13 @@
-/** Closing call-to-action: the productivity-engine pitch + install / docs buttons. */
+/** Closing call-to-action: the productivity-engine pitch + download / quick start / docs buttons. */
import { Link } from "react-router";
import { S } from "../lib/strings";
import { DOCS_URL } from "../lib/links";
import { Section } from "../components/section";
-import { ArrowRightIcon } from "../components/icons";
+import { DownloadIcon } from "../components/icons";
export function Cta() {
+ const secondary =
+ "inline-flex h-11 items-center rounded-lg border border-gray-200 bg-white px-5 text-sm font-medium text-gray-900 transition-colors hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:hover:bg-gray-800";
return (
@@ -17,16 +19,16 @@ export function Cta() {
diff --git a/packages/landing/src/sections/hero.tsx b/packages/landing/src/sections/hero.tsx
index 88fd6cf..b8b4c84 100644
--- a/packages/landing/src/sections/hero.tsx
+++ b/packages/landing/src/sections/hero.tsx
@@ -1,66 +1,20 @@
/**
* Hero: enlarged logo + product name, the one-line headline whose rotating word
* crossfades through a gaussian blur (Desktop <-> Server, localized per dictionary),
- * the one-sentence subtitle, the install one-liner behind an OS switcher, and stats.
+ * the desktop-first CTAs — a platform-aware download button pointing at /download
+ * (artifact resolution stays on that page), an all-platforms line and a link down to
+ * the quick start for CLI / self-hosted installs — and stats.
* The rotating word is a stacked inline-grid so line width never jumps.
*/
import { useEffect, useState } from "react";
import { Link } from "react-router";
import { S } from "../lib/strings";
-import { INSTALL_CMD, INSTALL_CMD_WINDOWS, REPO_URL } from "../lib/links";
-import { CopyButton } from "../components/copy-button";
-import { ArrowRightIcon, GitHubIcon } from "../components/icons";
+import { REPO_URL } from "../lib/links";
+import { detectPlatform } from "../lib/platform";
+import { ArrowRightIcon, DownloadIcon, GitHubIcon } from "../components/icons";
const ROTATE_MS = 2600;
-type InstallOs = "linux" | "macos" | "windows";
-
-/**
- * Install one-liner behind an OS tab row: one command visible at a time (Linux and
- * macOS share the POSIX one-liner; the tabs still name them apart so nobody has to
- * know that). Prompt char follows the shell: $ for POSIX, > for PowerShell.
- */
-function InstallBox() {
- const [os, setOs] = useState
("linux");
- const cmd = os === "windows" ? INSTALL_CMD_WINDOWS : INSTALL_CMD;
- const osBtn = (active: boolean) =>
- `rounded-md px-2.5 py-1 text-xs font-medium transition-colors ${
- active
- ? "bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-gray-100"
- : "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
- }`;
- return (
-
-
- {(["linux", "macos", "windows"] as const).map((key) => (
- setOs(key)}
- >
- {S.install[key]}
-
- ))}
-
-
-
-
- {os === "windows" ? ">" : "$"}
-
- {cmd}
-
-
-
-
- );
-}
-
function RotatingWord({ words }: { words: string[] }) {
const [active, setActive] = useState(0);
useEffect(() => {
@@ -86,6 +40,15 @@ function RotatingWord({ words }: { words: string[] }) {
}
export function Hero() {
+ // Platform-aware label only; every link goes to /download, where the platform
+ // cards and the GitHub/OSS artifact resolution live.
+ const detected = detectPlatform();
+ const downloadLabel = detected
+ ? S.hero.downloadCtaFor(S.download.platforms[detected].name)
+ : S.hero.downloadCta;
+ const textLink =
+ "inline-flex items-center gap-1 text-brand-700 underline decoration-brand-300 underline-offset-2 transition-colors hover:text-brand-600 dark:text-brand-300 dark:decoration-brand-700";
+
return (
@@ -125,10 +88,17 @@ export function Hero() {
style={{ animationDelay: "200ms" }}
>
- {S.hero.ctaPrimary}
+
+ {downloadLabel}
+
+
+ {S.hero.ctaQuickstart}
-
-
-
{S.hero.installHint}
-
+
+ {S.hero.downloadAll}
+
+ ·
+
+ {S.hero.cliAlt}
+
+
>
)}
- {/* Below both install methods: the desktop app replaces this whole flow for
- users who never want a terminal, so it stays visible on every switch. */}
+ {/* Below both install methods: the desktop app is the headline install path
+ (hero + /download); this note tells CLI installers the two share one data
+ root, and still routes anyone terminal-averse back to the download page. */}
{S.install.desktopNote}{" "}