跳到内容

post.md

2026年10月23日 · 阅读约 1 分钟
日落时分一艘 Nacra 17 双体帆船驶过海面的动态 ASCII 艺术

本站是如何构建的

快速了解 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 文件。

代码块可以带上文件标题:

src/features/blog/embeds/index.tsx
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/元数据、可观测性、按键输入

下面是撰写本文时各文件夹中的源文件数量:

各文件夹的源文件数
查看数据
folderfiles
app17
components16
lib16
features15
i18n4
hooks3
types1

生成的图标与社交卡片

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 在水合之前运行,记录客户端错误、网页性能指标和路由变化。两者都把事件交给一个上报器,由它分发到多个接收端:

src/lib/observability/server.ts
export const serverReporter = createReporter([
  consoleSink,
  ...(endpoint ? [endpointSink(endpoint)] : []),
])

客户端事件会发送到 /api/observability,由它记录下来。以后要接入真正的收集服务,只需增加一个接收端,而无需改动这两个入口文件。

翻译

网站提供十种语言,包括从右向左书写的阿拉伯语和希伯来语。每种语言都有各自的消息文件,保存所有文字;如果某个键缺失,就会把默认语言的消息垫在下面合并,因此页面不会出现空白。文章的处理方式相同:每种语言有各自的文件,缺失时回退到英文。

普通~/blog/how-this-site-is-built
0%

键盘快捷键

12
主导航
jk
向下 / 向上滚动
g gG
跳到顶部 / 底部
np
较新 / 较早的文章
d
切换主题
:
打开命令面板
?
显示 / 隐藏此帮助
esc