为什么
这个博客的旧版是那种大街上随处可见的深蓝色主题——蓝底白字、圆角卡片、16px 的系统默认字体。你随便打开十个开发者的博客,至少有六七个长这样。它的设计没有透露出关于作者的任何信息——你不知道写这些文章的人是一个终端美学的偏执狂,还是一个用 Notion 模板五分钟搭出来的页面。Open Graph 卡片看起来和其他所有开发者博客一模一样——白底黑字加一个标题,在 Discord 的信息流里你根本区分不出来这是谁的链接。
我想要一个设计,能够承受住被人在 Discord 频道里截图、裁剪、压缩、然后转发到五个不同的群聊之后,仍然能让人一眼看出来"这是有意为之的"。它不是随便选的,它是被设计过的。
我研究了两个参考对象。Nous Research 的暗色表面处理——它不是那种泛泛的 #1a1a2e 深蓝,而是一种近乎纯黑的、带有微弱暖色调的灰黑,像一个被擦过的黑板。然后是 hermesagent.ai 的粗野主义风格——没有任何阴影、没有任何圆角、没有任何渐变、没有任何"让东西看起来好看"的装饰性元素。每一个视觉元素存在的唯一理由是它提供了信息。我从这两个参考中各取了一部分:Nous 的底色处理——纯黑(但不是 #000000,是比纯黑微微亮一级的 #0a0a0a,这样当你在上面画一条纯黑的线时线是可见的),hermesagent 的粗野主义克制——全站一个强调色、全站等宽字体、每一个区块都有标签说明它是什么。
1. CSS 属性系统
十三个自定义属性全部定义在 :root 伪类上,没有在任何更深层级的选择器中重新声明 CSS 变量。这意味着这十三个值是全站唯一的、不可覆盖的——你在任何嵌套的组件中引用 var(--cyan),拿到的永远是一模一样的 #06b6d4。调色板被组织成一个五级表面色阶梯加上六个有严格语义约束的强调色:
| 变量 | 值 | 职责 |
|---|---|---|
--void | #0a0a0a | 页面背景。不是 #000,而是比纯黑亮大约 4% 的极暗灰色——这样当你在它的上面放一个纯黑边框时,边框是可见的 |
--void-1 | #0f0f12 | 第一级抬升表面。用于卡片容器、设置面板背景。比 void 亮一个级别,刚好够区分"内容"和"背景" |
--void-2 | #15151a | 第二级抬升表面。用于 blockquote 引用块、代码块 <pre> 的背景。再往上就没有更亮的表面色了——三级表面色对于一个博客来说已经够了 |
--fg | #f7ede3 | 主文字颜色。暖调的米白色,不是冷调的纯白——纯白在暗色背景上太过刺眼,而且会给人一种"这是一份 PDF 文档"的疏离感。暖白让文字看起来像是印在纸上的,而不是显示在屏幕上的 |
--fg-2 | #d4cfc4 | 次要文字颜色。用于链接的默认颜色、小标题、辅助说明文字。比主文字暗两个级别,刚好在"能看清但不抢视线"的临界点上 |
--muted | #a0a0a0 | 三级文字颜色——元数据标签(OUTPUT、SEED、DATE 这些)、时间戳、副标题行、表格中不太重要的列。这个灰色和背景的对比度刚好满足 WCAG AA 的标准(对比度约 4.5:1),但不会吸引注意力 |
--cyan | #06b6d4 | 全站唯一的强调色。链接、<code> 内联代码、选中文本的高亮背景、导航栏中当前页面的指示器、以及终端提示符 $ 的颜色。青色的选择不是随机的——它在 #0a0a0a 的背景上有大约 7:1 的对比度,完全满足无障碍标准,而且在几乎所有色盲模拟中仍然能被区分出来 |
--violet | #8b5cf6 | 悬停强调色——被用在链接的 hover 状态和语法高亮中的关键字。为什么不用青色做 hover?因为如果 hover 还是青色的,用户不太容易分辨"这个链接当前是不是被悬停着"。换一个不同的色相——从青色的互补方向取一个紫色——产生了明确的视觉差异 |
--green | #22c55e | 严格限制使用范围——只用于终端提示符 $ 和成功状态的指示(比如"保存成功"的通知)。这是故意设置的限制:如果绿色被用在任何地方——一个标题、一个强调段落、一个装饰性边框——那它的语义就稀释了。当你看到绿色的时候,你就知道这代表"成功"或者"命令可以执行了" |
--red | #ef4444 | 同样严格限制——只用在小部分错误指示上。404 页面上的文字、表单验证失败的边框 |
--amber | #f59e0b | 数据强调色。在语法高亮中被分配给数字字面量——整数、浮点数、十六进制。琥珀色在所有表面色背景上都有很好的对比度,而且它和青色、紫色都不冲突 |
--steel | #475569 | 边框和分隔线的默认颜色。这个灰色刚好在"作为一个分隔线能被看到"和"不吸引注意力"的中间地带 |
--steel-2 | #334155 | steel 的深色变体,用于 hover 状态下的边框加深 |
2. 排版:纯等宽字体,显式禁用连字
字体栈是 'Sarasa Mono SC', 'JetBrains Mono', 'SF Mono', Menlo, Monaco, Consolas, 'Liberation Mono', monospace。注意这个顺序是有讲究的——中文用户优先使用 Sarasa Mono SC(一个专门为编程和终端场景设计的等宽中文字体),其次是 JetBrains Mono(大多数开发者的机器上都有,因为 JetBrains IDE 自带这个字体),然后是 macOS 系统的 SF Mono,再然后是跨平台的备选字体。整个字体栈的目的是零网络开销——所有这些字体要么是系统预装的,要么是通过 IDE 附带安装的,不需要从 Google Fonts 或者其他 CDN 加载任何字体文件。页面上没有一个字节的字体下载。
博客中的每一个元素——大标题、小标题、正文段落、内联代码、代码块、导航链接、页脚文字、甚至表格里的内容——全部使用这个字体栈。没有任何一个元素使用不同的字体。这是故意的——在一个终端里,你不会看到 serif 的标题和 monospace 的正文混排。终端就是终端,从头到尾一个字体。
连字(ligatures)在现代编码字体中是一个常见特性——比如 -> 会被合并成一个箭头符号 →,!= 会被合并成 ≠,>= 会被合并成 ≥。这些在 IDE 里写代码的时候很好用,因为它们是语义正确的简写。但在博客的正文阅读语境中,连字是反作用的——当你看到 != 被渲染成 ≠ 的时候,这不是"不等于"的清晰表达,而是"我的字体在替我改写我写的文字"。CSS 显式关闭了这些特性:
body {
font-feature-settings: "calt" 0, "liga" 0;
font-variant-ligatures: none;
}
"calt" 0 关闭了上下文替换——就是那种根据前后字符自动选择不同字形的"智能"渲染。"liga" 0 关闭了标准连字。font-variant-ligatures: none 是更高层级的重置,确保即使浏览器实现不同,结果也是确定的。
基础字号是 14px,行高是 1.65。选择 14px 的原因:它在 820px 最大宽度的容器里恰好让每行容纳大约 75 到 85 个英文字符——这是印刷排版学中公认的最佳阅读行长。行高 1.65 比标准的 1.5 略宽——在中英文混排的段落中,中文的视觉高度比英文略高,需要额外的行间距来防止上下行文字在视觉上互相挤压。
3. CRT 扫描线 + 网格叠加层
两个固定的伪元素以极高的 z-index(999 和 1000)覆盖在整个页面上方,都设置了 pointer-events: none 来确保它们不会拦截任何用户的交互——点击、滚动、文本选择全部正常穿透到下面的内容。这两个伪元素是纯视觉的:
- 扫描线(通过
body::before实现):使用repeating-linear-gradient创建的水平条纹——白色在 2.5% 不透明度下的一条 1px 高的线,每 3px 重复一次。第一次尝试的时候用的是 5% 的不透明度——结果在深色背景上的扫描线太过突兀,正文文字看起来像被细密的百叶窗切割过,阅读体验非常差。降到 2.5% 之后的效果是:你不是在"看"到这条扫描线,你是在"感觉到"它——它给整个页面增加了一种微妙的纹理质感,像是一张印在略粗糙的纸张上的文档,而不是一个像素完美渲染的 LCD 屏幕。这种质感在长时间阅读的时候有实际的心理效果——它让页面看起来不那么"冷"和"数字"。 - 网格(通过
body::after实现):使用另一个repeating-linear-gradient创建的 64px 方形青色网格,1.8% 的不透明度。这个网格参考了老式 CAD 软件和 PCB 布局工具的界面——在这些工具中,深色背景上总是覆盖着一层淡淡的网格,作为视觉参考线和测量辅助。在这个博客上,网格当然没有任何实用的测量功能,但它的视觉存在感——尽管几乎是看不见的——让页面的留白区域不再是一片虚无的纯黑色,而是有了微弱的空间结构。如果哪天我把它去掉,你不会注意到少了什么,但你会觉得"页面好像哪里不太对了"。
4. 链接设计:color-mix 下划线
链接的默认下划线不是标准的 text-decoration: underline——那条 100% 不透明、和文字颜色一样的实线。它用的是 color-mix() 这个比较新的 CSS 函数:
a {
text-decoration-color: color-mix(in srgb, var(--cyan) 40%, transparent);
}
color-mix(in srgb, var(--cyan) 40%, transparent) 的意思是:在 sRGB 色彩空间中,把青色和完全透明混在一起,青色的比例是 40%。结果就是一条 40% 不透明度的青色下划线——它不够显眼到让你觉得"这里有一条线",但你余光扫过去的时候能感知到"这个文字下面有东西"。当鼠标悬停在链接上时,下划线的颜色从青色变为紫色(var(--violet)),不透明度从 40% 跳到 100%,下划线的粗细从 1px 过渡到 2px。过渡时间是 150 毫秒,同时作用在 color、text-decoration-color 和 text-decoration-thickness 三个属性上。
键盘聚焦(:focus-visible)获得的不是下划线而是一个 1px 的青色虚线外轮廓,偏移元素边缘 3px——虚线轮廓比实线轮廓更不容易被误认为是"这个元素的边框"。文本选中状态的样式是:青色背景 + void 色文字——不是高亮(加亮),而是反转(对调背景和文字的颜色)。
5. 终端导航栏
页面顶部的导航栏模拟了终端提示符的视觉模式:
$ ~/blog [HOME] | [ARCHIVE] | [ABOUT] | [DOCS] | [RSS]
左边是一个绿色的 $ 符号——这是 Unix shell 的标准提示符,绿色表示"准备好接受命令了"。紧跟着的是一个 muted 色的路径字符串——在首页上显示 ~/blog,在博文页面显示 ~/post-slug,在文档页面显示 ~/docs/project/section。右边是一组用方括号包裹、竖线分隔的链接——[HOME]、[ARCHIVE]、[ABOUT]、[DOCS]、[RSS]。每个链接是一个药丸形状的按钮——带有 1px 的透明边框,所以默认状态下你看不到边框,只有文字。当鼠标悬停时,透明边框变成 steel 色,文字颜色从默认的 fg-2 变成青色。这给人一种"这个按钮从隐形变成了可见"的微妙反馈。
导航栏是页面上唯一一个会根据导航状态变化的元素——当前页面对应的链接使用青色文字和可见的边框来表示"你在这里"。其余所有元素都是纯内容,没有任何交互状态。
6. 博文列表设计:虚线边框
首页和归档页中的每一条博文记录都是一个 <li> 元素,它的样式是:
border: 1px dashed var(--steel)——虚线边框,没有圆角,没有背景填充。这个虚线边框让博文列表看起来像是一排标签化的档案记录——就像是文件柜上的标签,而不是"被设计过的博客卡片"- 悬停状态下,虚线边框变成实线青色,博文标题的颜色变为青色。没有背景颜色变化,没有阴影投射,没有位移动画。"卡片抬升"是一种通用的 UI 模式——卡片在悬停时微微上浮并投射阴影——但在终端美学中,东西不会"浮起来",它们只会改变颜色
- 过渡时间是 150 毫秒,只作用于
border-color和color两个属性
7. 元数据行:OUTPUT + SEED + DATE + CHARS + TAGS
每篇博文页面在标题下方显示一行元数据,格式是大写的小号文字,每个字段有标签和值:
- OUTPUT——博文的发布序号,三位补零(001、002、003……)。这个值不是手动填入的——它是从
POSTS数组中博文的索引位置确定性计算出来的。它模仿了终端中ls或者find命令输出的编号风格 - SEED——对
slug|date这个拼接字符串做 FNV-1a 哈希,取前 4 字节,以无符号 32 位整数显示。同一篇博文的同一个版本永远产生同一个 SEED 值。SEED 的存在意义两重——一是视觉上的(它让元数据行看起来更像某种系统输出,而不是"博客的发布时间"这种常规信息),二是功能上的(如果你看到一张 OG 卡片上的 SEED 值和你独立计算出来的不一样,你就知道有人篡改了卡片内容) - DATE——博文的发布日期,YYYY-MM-DD 格式,从 POSTS 记录中读取
- CHARS——博文正文的字符数(不含 HTML 标签),按浏览器 locale 格式化(带千位分隔符)
- TAGS——逗号分隔的标签列表,每个标签是一个带有虚线边框的小药丸
标签使用 muted steel 色(--muted),值使用略暗的白色。在移动端(视口宽度小于 640px),元数据行启用 flex-wrap 换行并缩小 letter-spacing——原本的 letter-spacing: 1.5px 在窄屏幕上会导致元数据行被迫堆成三行甚至四行。缩小到 0.8px 之后刚好两行放下。
8. 块级元素样式
- Blockquote 引用块:2px 实线青色左边框,背景色提升到
--void-2(第二级表面色)。没有引号装饰,没有斜体——左边框本身就是最清晰、最不侵入正文的引用指示器。它说"这一段是从别处引用的",而不需要在你阅读的时候强行往文字上附加额外的视觉噪音 - 代码块:
<pre><code>包裹,使用--void-2背景、steel 色边框。右上角浮动着一个语言标签——从class="language-ts"这个属性自动提取,显示为// typescript或# python的格式(根据语言不同选择//还是#作为注释前缀)。这个标签不是多余的装饰——当你在看一段代码的时候,一眼看到右上角的// typescript,你的大脑会自动切换到 TypeScript 的语法模式,不需要手动去"猜这是什么语言" - 表格:全宽、边框折叠(
border-collapse: collapse)、表头行用 steel 色底部边框、数据行交替背景(--void-1和--void交替)。表格没有外边框——网格线自身定义了表格的结构,不需要再画一个外框把网格包起来 - 列表:无序列表使用青色圆点标记(
::marker { color: var(--cyan) }),padding-left: 1.5rem的缩进。设置类列表(连续的多项设置说明)中,列表项之间用虚线分隔——这给密集的信息提供了视觉上的呼吸节奏
9. 语法高亮:正则,4 个 Token
语法高亮是在 Cloudflare Worker 的渲染阶段执行的——不是在前端浏览器中。当 Worker 生成一个页面的时候,它扫描所有的 <pre><code class="language-X"> 代码块,根据 class 属性选择对应的语言规则(目前支持 TypeScript/JavaScript、Python、Shell、JSON、YAML,不支持的语言退回到无高亮的纯文本),然后用正则表达式在 HTML 转义后的文本上匹配并包裹 token。
整个高亮器定义了四种 token 类型,分别对应四个 CSS 颜色类:
- 关键字(
.c-kw,紫色):匹配语言的关键字列表——TypeScript 中包含const|let|var|function|return|if|else|for|while|class|import|export|default|from|async|await|try|catch|throw|new|this|true|false|null|undefined等大约 40 个常见关键字 - 字符串(
.c-str,青色):匹配单引号、双引号和模板字符串(反引号)中的内容。使用(["'`])(?:\\\.|(?!\1).)*\1这个正则来处理转义字符——它确保像"he said \"hello\""这样的字符串能被完整匹配,不会在中间的转义引号处断开 - 注释(
.c-cmt,斜体 muted 灰色):对于类 C 语言匹配//到行尾和/* */多行注释,对于 Python 和 Shell 匹配#到行尾 - 数字(
.c-num,琥珀色):匹配整数(\b\d+\b)、浮点数(\b\d+\.\d+\b)和十六进制(0x[0-9a-fA-F]+)
四个正则的执行顺序非常关键——注释必须最先匹配。为什么?因为注释里面的关键字不应该被高亮:// this function returns a const 里面的 function 和 const 是注释的一部分,不是代码。如果关键字的正则在注释之前执行,这句话里的 function 和 const 就会被错误地染成紫色。类似的,字符串必须在数字之前匹配——"version 2.0" 里面的 2.0 是字符串内容,不是数字字面量。
这种基于正则的方案有已知的局限性——字符串内部的注释标记不会被正确区分(比如 const x = "this is not a // comment" 中的 // comment" 会被错误地识别为注释)。但对于一个所有代码块都是手工编写且长度通常在 5 到 20 行之间的博客来说,这个代价完全可以接受——你不可能在这些代码块里写出需要 AST 解析器才能正确处理边缘情况的复杂嵌套语法。换来的好处是巨大的:零外部依赖、运行时纯字符串操作、在所有页面上的渲染结果完全一致。
10. Open Graph 卡片:通过 Cloudflare Worker 动态生成 SVG
博客的每个页面——首页、每一篇博文、每一个文档页面——都有一个独一无二的 og:image,位于 /og/{slug}.svg。这些不是预先渲染好存在服务器上的静态 PNG 文件——它们是同一个 Cloudflare Worker 在收到请求的时候实时生成的 SVG 图像。
OG 卡片的视觉设计沿用了博客的风格:纯黑底色(#0a0a0a)、青色虚线边框(1200×630 的 viewBox,32px 的内边距)、左上角标注 OUTPUT 001(博文序号)、右上角标注 SEED: 1581663009(内容哈希的 uint32 表示)、中间区域展示博文标题(使用等宽字体,按 30 字符自动换行,字号根据标题行数自适应——单行用 72px,双行及以上用 52px)、底部分别用青色和灰色标注标签列表和发布日期、最底部是 64px 的青色网格背景(8% 不透明度)。
Worker 处理 /og/*.svg 的路由逻辑很简单——从 URL 中提取 slug,在 POSTS 数组中查找对应的博文,提取标题、标签和日期,生成一个包含内嵌字体的 SVG 文档。字体是以 base64 编码的 data URI 形式嵌入在 SVG 内部的,这意味着接收这个 SVG 的平台——Twitter、Discord、Telegram、iMessage——不需要安装 JetBrains Mono 字体就能正确渲染卡片上的文字。
SEED 值使用 FNV-1a 哈希算法从 slug|date 这个拼接字符串计算而来。这个哈希是确定性的——同一个博文在所有部署中永远产生同一个 SEED。如果任何人独立计算了这个哈希值,发现和卡片上显示的不一致,他就能判定这张卡片的内容被篡改过。
11. 结构化数据:带 BlogPosting Schema 的 JSON-LD
每篇博文页面在 HTML 的 <head> 中嵌入一个 <script type="application/ld+json"> 块,内容是符合 Schema.org 规范的 BlogPosting 结构化数据。Google 和 Bing 的爬虫通过解析这个 JSON-LD 块来理解页面的语义结构,用于生成富文本搜索结果(比如在搜索结果中显示作者头像、发布日期、文章摘要)。包含的字段:headline(博文标题)、description(博文的 excerpt 字段,截断到 155 字符)、datePublished 和 dateModified(发布日期)、author(一个 Person 类型,带有指向 GitHub 个人页的 sameAs 属性)、publisher(同样是 Person 类型)、mainEntityOfPage(当前页面的规范 URL)、image(OG 卡片的 URL)、inLanguage(en-US 或 zh-CN,根据当前语言自动切换)、keywords(逗号分隔的标签列表)、articleSection(固定为 "Engineering")、wordCount(通过去除 HTML 标签后按空格分词计算出的单词数)。
首页使用的是 WebSite schema,包含一个 SearchAction 的 potentialAction——这让 Google 可以在搜索结果中为你的博客展示一个搜索框。这是在不引入过度设计的前提下能拿到的最小的结构化数据足迹。
12. RSS 和 Sitemap:从 POSTS 数组自动生成
/rss.xml 和 /sitemap.xml 都不是手动维护的静态文件——它们是从驱动整个博客的同一个 POSTS 数组在请求时动态生成的。不需要单独的配置文件,不需要每次发新文章时手动更新 sitemap。
RSS feed 包含每篇博文的完整正文(包裹在 <content:encoded> 的 CDATA 块中),以及正确的 pubDate 格式(使用 UTC 中午 12:00 作为基准时间,再根据每篇博文在数组中的索引偏移对应秒数,保证按发布日期排序时顺序是确定的)、Dublin Core 的 <dc:creator> 标签、Atom 的 <atom:link> self-link。Channel 级别的元数据包括 language、lastBuildDate、generator、managingEditor、webMaster、copyright 和 ttl(60 分钟——告诉 RSS 阅读器每 60 分钟检查一次更新)。
Sitemap 包含 57 个 URL——5 篇博文、38 个文档页面(6 个项目 × 平均 6-7 个章节)、3 个静态页面(首页、关于、归档)、以及 10 个图片 Sitemap 扩展条目(将 OG 卡片 SVG 作为图片提交给搜索引擎的图片索引)。每个 URL 都带有 lastmod(最后修改日期)、changefreq(更新频率——博文是 monthly,文档是 weekly,首页是 daily)和 priority(优先级)三个字段。
13. 构建:零客户端 JavaScript
整个博客是一个单一的 Cloudflare Worker:export default { async fetch(req) { ... } }。大约 46 KB 的 JavaScript 源代码(压缩后更小)。运行时没有任何 npm 依赖——所有代码都是手写的,没有引入任何第三方库(连 express 或者 itty-router 都没用,路由就是一堆 if (path === "/xxx"))。Worker 不向浏览器传输任何 JavaScript——HTML 输出中没有任何一个 <script> 标签指向外部脚本或内联脚本。页面就是纯 HTML 加上内联 CSS,浏览器收到之后就完成了——不需要再发任何额外的网络请求来加载字体、图标、统计脚本或分析工具。
Worker 处理的所有事情:URL 路由 → 页面渲染(pageWrap 函数,接受 title、description、extraHead 等参数生成完整的 HTML 页面)→ 博文列表渲染 → 语法高亮 → OG 卡片生成 → RSS 和 Sitemap 生成 → 静态资源(favicon 是一个内联的 data URI SVG)。所有这一切都在一次 HTTP 请求-响应周期内完成,在 Cloudflare 全球边缘节点的 V8 隔离环境中执行,延迟通常在 5-15 毫秒之间。这比浏览器加载这个页面的 favicon 还要快。
14. 响应式设计:两个断点
设计上只有两个显式的断点:640px 和 400px。
在 640px 以下(手机竖屏):容器的水平内边距从 2rem 缩减到 1rem(因为你的屏幕本身就只有 375px 宽,2rem 的内边距会吃掉超过 10% 的内容宽度)。元数据行启用 flex-wrap 并缩小 letter-spacing——从 1.5px 降到 0.8px——否则六个带标签的数据项在 375px 的屏幕上会强制堆成三排。导航栏的链接垂直堆叠而不是水平排列。
在 400px 以下(极窄屏幕,比如 iPhone SE 第一代):导航栏中方括号和竖线分隔符被隐藏,只保留纯文字的链接。因为方括号和竖线在极窄屏幕上占用了太多水平像素。
在 820px 以上没有断点——容器的 max-width 就是 820px,水平居中。在超宽屏幕上(比如 3440px 的带鱼屏),你的视线集中在屏幕中间 820px 的一列内容上,两侧是无尽延伸的 void 黑色。扫描线和网格覆盖层不限于内容宽度——它们覆盖整个视口,无论你有多宽的屏幕上都能填满。这种做法在视觉上强调了"这个页面不是一个矩形的画布,它是一个无限黑暗空间中的一个岛"。
15. 验证
我在三个视口尺寸下用 Playwright 的无头浏览器模式截了每一页的图——390px(iPhone 14 Pro)、820px(标准桌面)、1440px(外接显示器)——然后逐张对比我脑中的设计稿。发现了一个通过截图才能看到的问题:移动端窄屏幕上的元数据行在未启用 flex-wrap 之前会强行堆成三行——原本应该两行放下的六个标签项,因为 letter-spacing 过大被挤到了第三行。修复方法是在 640px 断点处启用 flex-wrap 并缩小 letter-spacing。
我接受的设计取舍
- 没有浅色模式。在户外强光下阅读会非常吃力。这是个人构建日志——我不需要在海滩上阅读它。
- 博文的 H1 大标题被替换成了终端风格的 slug 路径。每篇博文的 H1 不是"为什么又做一个课表应用",而是
> building-sleepy-a-material-you-schedule-app-with-jetpack-compose——把标题转换成了 kebab-case,在前面加上一个>符号,看起来像是一条cd命令。真正的标题文字显示在 H1 下方作为一个副标题。有些人会觉得这很蠢。它确实不是"正确"的信息层级——H1 应该是页面上最重要的标题——但它是一种签名。 - 语法高亮是基于正则的,不是基于 AST 的。前面已经解释过这个取舍。对于这个博客的使用场景来说,错误率在可接受范围内。
- 博文页面没有封面图片。OG 卡片 SVG 同时承担了两个职责——社交媒体的分享预览,以及事实上的博文视觉头图。如果为每篇博文单独设计和渲染专属的封面,会引入一个新的内容制作环节——这意味着每发一篇博文要多花至少 30 分钟在"设计封面"上。美学收益不值得这个额外的工作量。
- 在
prefers-reduced-motion媒体查询下,所有过渡动画被禁用。页面上唯一的动效就是链接悬停时的 150ms 颜色过渡。对于系统级开启了"减少动效"的用户来说,禁用这个过渡不需要任何代价,但可能对前庭功能敏感的用户产生实际的好处。
工作方式
这个设计是在两个晚上完成的。第一个晚上搭好了所有的结构性组件——调色板、字体栈、基本布局、导航栏的终端提示符模式、OUTPUT/SEED 元数据系统。第二个晚上清理了所有粗糙的边缘——修复了在极窄屏幕上元数据行从两行堆成三行的换行问题、修复了高亮器在 pre 块中错误转义 HTML 实体导致代码里的 < 和 > 显示为 &lt; 和 &gt; 的 bug、把扫描线的不透明度从过于刺眼的 5% 降到恰好可感知的 2.5%。
没有 tickets。没有 roadmap 上的 phase。没有方法论 slide。唯一的约束是:每一个设计决策,我都能用一句话说出它为什么是这个样子,不是别的样子。