落地页与主题
落地页是一个配置文件里的区块数组:可用的区块类型、四套内置主题,以及产品截图。
落地页是数据。src/config/landing.ts 导出一个区块数组,src/routes/_marketing/index.tsx 只负责渲染它——路由里既没有文案,也没有布局。整个应用(包括落地页)的外观来自主题:一组 CSS 自定义属性,内置四套。
修改落地页
landing.ts 里的内容全是模板自己的宣传,本来就是要换掉的。常见改动都只动一行:
| 想要 | 做法 |
|---|---|
| 调整区块顺序 | 在数组里移动一项 |
| 删除区块 | 删掉那一项 |
| 新增区块 | 按下面的某种区块类型加一项 |
| 修改文案 | 改 messages/en.json 和 messages/zh.json 里对应的消息 |
一个区块就是一个对象:一个 type,加上该类型的字段:
import { m } from '@/paraglide/messages'
export const landing: readonly LandingBlock[] = [
{
type: 'hero',
title: () => m.marketing_hero_title(),
subtitle: () => m.marketing_hero_subtitle(),
},
{
type: 'faq',
kicker: () => m.marketing_faq_kicker(),
title: () => m.marketing_faq_title(),
items: [
{ q: () => m.marketing_faq_cost_q(), a: () => m.marketing_faq_cost_a() },
],
},
{
type: 'cta',
title: () => m.marketing_cta_band(),
docs: () => m.marketing_cta_band_docs(),
},
]
每段文字都是函数
文案一律写成 () => m.key(),而不是 m.key()。配置模块在每个 Worker isolate 里只求值一次,而语言属于每个请求。直接调用会把最先加载这个模块的那个语言冻结下来,发给所有人。src/components/marketing/blocks/types.ts 里的 Text 类型会强制这一点,直接写字符串或直接调用都过不了 typecheck。见国际化。
底色(tone)
大多数区块有一个可选的 tone:'plain'(页面底色,默认)或 'sheet',后者会在区块下面垫一块圆角灰底。两者交替,长页面不用图片也有节奏感。有一条规则:自身卡片用 surface 色的区块——modules、bento、chips、pricing、faq、compare、gallery、testimonials——必须保持 plain,否则卡片会融进灰底里看不见。
区块类型
所有类型都是 src/components/marketing/blocks/types.ts 里的一个联合类型LandingBlock,每种类型在旁边各有一个实现文件。
| 类型 | 渲染内容 |
|---|---|
hero | 标题、副标题、主行动按钮和文档链接 |
logos | 一行或几行品牌标志,比如你的技术栈 |
logos-grouped | 分组展示的品牌标志 |
chips | 分组的名称标签,用于没有品牌标志的东西 |
stats | 大号数字,滚动到视野内时从零计数 |
terminal | 逐行展示的一段终端会话 |
steps | 带编号的步骤,每步配一个终端块 |
surfaces | 全宽的产品截图,随滚动切换 |
modules | 功能清单:每行一个名称加几个短标签 |
bento | 大小不一的卡片网格 |
compare | 对比表格 |
gallery | 别人用你的产品做出的东西;为空时什么都不渲染 |
testimonials | 滚动卡片上的用户评价 |
pricing | 你的套餐,读自 src/config/plans.ts |
faq | 可折叠的问答 |
cta | 收尾的行动号召 |
pricing 区块自己没有价格:它读的就是结账用的 src/config/plans.ts,所以页面上不会出现你并不收取的价格(见支付)。hero 和 cta 里主按钮的目标是 /dashboard;如果在 src/config/app-config.ts 里打开了 waitlist,则是/waitlist。
/blocks 区块样板
默认页面只用了部分类型。运行 bun run dev 后打开 /blocks,可以看到每种类型用示例数据渲染出来的样子。这是个工作台,所以里面的文案是普通英文字符串。把想要的对象复制进 landing.ts,再把字符串换成消息函数即可。生产构建里这个页面什么都不渲染。
新增区块类型
在 LandingBlock 联合类型里加一个成员,在 src/components/marketing/blocks/ 下新建一个组件文件,在 blocks.tsx 里导出,再在 render.tsx 的 switch 里加一个case。这个 switch 是穷尽的,漏掉 case 会是类型错误,而不是一个悄悄什么都不渲染的区块。
上线之前
默认页面带一个 testimonials 区块,里面是虚构的人和占位头像。把每一条都换成真实客户的原话(并取得对方许可),或者直接删掉这个区块。原样上线就等于发布虚构的评价。
截图与媒体
surfaces 区块展示的是应用的真实截图。它们是提交在 public/marketing/ 里的文件,每种明暗模式各一张:admin-light.webp 和 admin-dark.webp,依此类推。浏览器只会下载当前模式对应的那张。
UI 改动之后重新截图:
bun run dev # 在一个终端里
bun run capture:marketing # 全部截图;只截一张用 `bun run capture:marketing admin`
脚本会给本地数据库填充种子数据、用测试会话登录、用 Playwright 驱动真实应用,然后写出文件。后台页面显示的是你本地数据库里的任何数据,所以只要截图里出现了种子数据那几个假域名以外的邮箱地址,脚本就拒绝保存。如果装了 cwebp,它会把 PNG 转成 WebP。新增一个 surface,除了改 landing.ts,还要在 scripts/capture-marketing.ts 的 SHOTS里加一项。
对于不想放进 git 的大文件,比如视频,src/components/marketing/media.tsx 里的<Frame> 和 <Clip> 组件会从 appConfig.mediaUrl 的 marketing/ 前缀下读取。mediaUrl 为空(默认)时,它们渲染一个安静的占位块,而不是破图。bun run upload:marketing <dir> 会把一个目录里的图片缩放、转成 WebP,并用 wrangler上传。注意它上传到 wrangler.jsonc 里的第一个 bucket,也就是存放用户私有文件的那个;如果你要给它配一个公开域名作为 mediaUrl,请考虑为营销素材单独建一个 bucket(见文件存储)。
主题
src/config/themes.ts 里注册了四套内置风格:
| 主题 | 外观 |
|---|---|
shipkit(默认) | 中性配色与蓝,柔和圆角 |
editorial | 衬线字体,直角,纸张质感 |
soft | 淡紫,大圆角,柔和投影 |
brutal | 等宽字体,硬边框,高饱和黄 |
每套都有浅色和深色两面。两个设置相互独立地组合:
- 风格是
<html>上的data-theme属性,由src/components/theme-provider.tsx设置。读者的选择存在localStorage里,一小段内联脚本在首次绘制前就应用它,所以页面不会先闪一下默认主题。 - 模式(浅色或深色)是
darkclass,由 next-themes 负责。
读者可以在营销页页头的菜单里,或账号设置的“风格”一行里选择风格;模式有自己的切换按钮。
主题只是一组 token
默认主题写在 src/styles.css 的 :root 和 .dark 里;另外三套在src/styles/themes.css,位于 :root[data-theme="…"] 和:root[data-theme="…"].dark 之下。没有任何组件读取 data-theme。主题改变的一切都是CSS 自定义属性:--background、--primary、--brand 这样的颜色,以及形状 token:
| Token | 控制 |
|---|---|
--radius-btn | 按钮圆角 |
--radius-card | 卡片圆角 |
--radius-sheet | 大块底板和色带 |
--shadow-surface | 卡片、对话框、菜单的阴影 |
--shadow-raised | 按钮的阴影 |
--font-body | 正文字体 |
--font-display | 营销页大标题字体 |
所以手写按钮用的是 rounded-[var(--radius-btn)],而不是 rounded-full。如果新外观需要某个组件换形状,就为这个形状加一个 token,在每套主题里设置它,而不是在组件里写分支。
修改默认主题或新增主题
想换一种默认外观,改 src/config/themes.ts 里的 defaultTheme。如果只想要一种外观、不给选择,再把 ThemePicker 从 src/routes/_marketing.tsx 里去掉,并删掉src/features/auth/components/account-section.tsx 里的风格一行(ThemeRow)。
新增主题:在 themes 里加一项(id、名称和说明的消息,以及选择器用的三个色块颜色),再在 src/styles/themes.css 里写一个浅色块和一个深色块。防闪烁脚本是从注册表生成的,无需改动。默认外观的设计规则——颜色、圆角、按钮——写在 src/CLAUDE.md 里。