post.md
2026年10月23日 · 阅读约 1 分钟
本站是如何构建的
快速了解 cmkrw.com 的架构:从终端式布局,到 Markdown、图表和错误上报。
- nextjs
- design
目录
这个网站刻意保持小巧:一个页面、一个博客,以及尽可能少的 JavaScript。下面介绍各个部分如何配合,供你搭建类似的网站时参考。
终端就是布局
网站的布局就像一个终端应用。顶部是标签栏,底部是状态栏,中间的一切都是面板:带边框的方框,标题嵌在上边框中。一个小组件 Pane 就绘制了所有面板。
主从视图式的博客:切换文章时,列表保持不动。
导航也遵循同样的思路。在博客中,文章列表固定在左侧,文章在右侧打开,因此在文章之间切换从来不需要“返回”链接。快捷键的工作方式与终端中一样,你还可以点击列标题对下面这张表排序:
| 1 | 打开首页 | 全局 |
| 2 | 打开博客 | 全局 |
| j | 向下滚动 | 全局 |
| k | 向上滚动 | 全局 |
| g g | 跳到顶部 | 全局 |
| G | 跳到底部 | 全局 |
| n | 较早的文章 | 博客 |
| p | 较新的文章 | 博客 |
| d | 切换主题 | 全局 |
| ? | 显示快捷键 | 全局 |
动效主要靠 CSS 实现。首屏的命令用 steps() 动画逐字输入,面板在滚动进入视野时浮现,标签栏下方的线条和状态栏中的百分比都跟随滚动位置变化。在 prefers-reduced-motion 下,所有动效都会关闭。
撰写文章
文章是位于 content/blog/<slug>/<locale>.md 的纯 Markdown 文件,由 TanStack Markdown 渲染。缺少某种语言的翻译时会回退到英文文件,而 draft: true 会让文章不出现在生产构建中。
其余工作由三个 TanStack 工具完成:
- Markdown 将文章解析并渲染为 React 元素,中间不经过 HTML 字符串。
- Highlight 为代码块着色。只会打包我注册的语言,颜色来自与主题其余部分相同的 CSS 变量。
- Charts 和 Table 通过一行注释嵌入,注释中指明文章旁边的一个 JSON 文件。
代码块可以带上文件标题:
export function createEmbeds(slug: string, locale: string) {
return {
chart: ({ data = "", kind = "bar", x = "x", y = "y", title = "Chart" }) => (
<TuiChart
rows={getPostDataset(slug, data, locale)}
kind={kind}
x={x}
y={y}
label={title}
/>
),
}
}普通的 Markdown 表格同样可用,并会采用相同的样式:
| 层级 | 文件夹 | 职责 |
|---|---|---|
| 路由 | app/ | 页面、元数据、图标和图片 |
| 界面 | components/tui/ | 面板、导航栏、状态栏、快捷键映射 |
| 功能 | features/ | 首页各部分与博客 |
| 工具 | lib/, hooks/ | 元数据、可观测性、按键输入 |
下面是撰写本文时各文件夹中的源文件数量:
查看数据
| folder | files |
|---|---|
| app | 17 |
| components | 16 |
| lib | 16 |
| features | 15 |
| i18n | 4 |
| hooks | 3 |
| types | 1 |
生成的图标与社交卡片
Next.js 16 根据 app/ 目录中的文件生成元数据图片:
icon.tsx和apple-icon.tsx根据同一个 SVG(assets/logo-icon.svg)生成网站图标和 iOS 图标。opengraph-image.tsx为首页生成社交卡片,另有一个为每篇文章生成卡片。
并不存在 favicon.tsx。文档明确指出 favicon 无法生成,所以由 icon.tsx 来完成这项工作。这些都共用一小套基础元素,因此修改品牌标志会同时更新网站图标和所有社交卡片。
监控埋点
有两个文件负责可观测性。instrumentation.ts 在服务端运行并上报请求错误;instrumentation-client.ts 在水合之前运行,记录客户端错误、网页性能指标和路由变化。两者都把事件交给一个上报器,由它分发到多个接收端:
export const serverReporter = createReporter([
consoleSink,
...(endpoint ? [endpointSink(endpoint)] : []),
])客户端事件会发送到 /api/observability,由它记录下来。以后要接入真正的收集服务,只需增加一个接收端,而无需改动这两个入口文件。
翻译
网站提供十种语言,包括从右向左书写的阿拉伯语和希伯来语。每种语言都有各自的消息文件,保存所有文字;如果某个键缺失,就会把默认语言的消息垫在下面合并,因此页面不会出现空白。文章的处理方式相同:每种语言有各自的文件,缺失时回退到英文。