Jonas Zhou

这个网站的技术解析

2026年7月1日

从 Next.js 前端、内容系统、Route Handlers、外部数据接入到构建和托管,整理这个个人博客的技术结构。

本文大部分由 AI 生成,并由我审阅和编辑。

一句话概括:

这是一个基于 Next.js App Router 的个人博客:文章以 MDX 文件管理,页面由 React 组件渲染,少量动态数据来自 Notion 和非官方 API,部署在 Vercel 上,并对关键数据层使用 Cache Components 缓存。

前端

前端主体是 Next.js 16 + React 19 + TypeScript,使用 App Router 与 file-based routing。核心路由包括:

  • /:首页,展示介绍、推荐页面卡片、Micro Logs 和文章列表。
  • /about:个人简介。
  • /projects:项目索引。
  • /{year}/{slug}:正式博客文章,例如 /2026/AI-B2BB-CRM
  • /psychology-effects:心理学效应集合(Notion 数据源)。
  • /micro-logs:轻量日志流(Notion 数据源)。
  • /org-chart-web:组织架构网页编辑工具。
  • /links/[id]:带 Open Graph 元数据的智能跳转链接。
  • /atom:Atom Feed。

UI 层主要由 Tailwind CSS 3、自定义 React 组件,以及 MDX content components 组成。Mermaid、Tweet、Bilibili、YouTube 这类重交互或外部嵌入模块按需加载;Mermaid 保持 Client Component,Tweet / Bilibili 则在服务端拉取数据后再渲染。

布局按用途分成三个 route group:

Route Group路径示例用途
(post)
/(post)/2026/about-this-site
博客文章,带 TOC 和文章导航
(page)
/(page)/psychology-effects
专题页 / 工具页
(no-layout)
/(no-layout)/org-chart-web-editor
无站点主布局的全屏编辑器

这个前端不是 landing page 型结构,而更像一个长期使用的阅读界面:首页负责入口和索引,文章页负责深度阅读,专题页负责展示 Notion 同步的数据或交互工具。

内容系统

内容层不是 headless CMS,而是 file-based MDX + JSON 索引

当前主要类型:

类型Source索引用途
posts
app/(post)/{year}/{slug}/page.mdx
app/index-posts.json
正式博客文章
featured pages
app/(page)/*/page.mdx
app/index-featured.json
首页横向卡片推荐
static pages
app/about/page.mdxapp/projects/page.mdx
顶层说明页
notion pages
Notion database
代码内硬编码 pageId
Micro Logs、心理学效应

每篇文章是一个独立目录,内含 page.mdx,可选 components.tsxnotion.ts。这意味着单篇文章可以加载任意模块、拥有自定义布局,并利用 Next.js 自动代码分割——单篇的冗余不会拖慢全站。

索引文件维护文章和推荐页的元数据(iddatetitleurlorder)。页面通过 getPosts()getFeaturedPages() 读取索引,而不是在构建时扫描文件系统。

新建文章可使用 pnpm new-post 脚本:交互式输入标题和描述,自动生成 MDX 骨架,并将条目写入索引(中文标题可经腾讯云翻译 API 转为英文 slug)。

Markdown 管线

Markdown 构建管线基于 @next/mdx,配置在 next.config.js

  • pageExtensions 包含 mdmdx,MDX 文件可直接作为页面。
  • experimental.mdxRs: true:启用 Rust 版 MDX 编译器,加快编译速度。
  • cacheComponents: true:启用 Next.js 16 Cache Components 模型。

mdx-components.ts 将 MDX 元素映射到自定义组件,构成站点的样式指南:

类别组件
排版
H1 / H2 / H3PAOL / UL / LIBlockquoteHR
代码
CodeSnippet<pre> 替换)
媒体
ImageFigureCaption
嵌入
TweetYouTubeBilibiliMermaid
辅助
CalloutDataTable、脚注(Ref / FootNotes / FootNote
中文字体
Kai(楷体)、Song(宋体)、Hei(黑体)、JHSong(京华老宋体)

标题锚点支持 [#custom-id] 写法:在标题末尾标注自定义 id,H1/H2/H3 组件会自动生成可见的 # 链接。文章页左侧的 TOC 组件在客户端扫描 article 内的标题,优先读取这类自定义锚点。

字体与主题

字体策略兼顾西文与中文排版:

  • 西文:Inter(正文)、Kanit(Logo)、Roboto Mono(等宽)。
  • 中文:方正楷/宋/黑(cn-fontsource)、更纱黑体 Mono SC(子集化,仅保留常用汉字)、京华老宋体(子集化)。
  • 子集化脚本位于 scripts/subset-sarasa-font.jsscripts/subset-jhsong-font.js,产物在 public/fonts/

主题切换通过内联 themeEffect 脚本在首屏渲染前执行,支持 light / dark / system 三态,并配合 localStorage 持久化。Vercel Analytics 与 Speed Insights 在根布局注入。

后端

服务端能力由 Next.js Route Handlers 提供,代码在 app/api/ 和若干 route.ts 文件:

入口用途
app/api/posts/route.ts
返回文章列表 JSON
app/api/featured/route.ts
返回推荐页列表
app/api/posts/view/route.ts
文章浏览量(框架已搭建,incr 待实现)
app/api/pages/view/route.ts
页面浏览量
app/api/bilibili/route.ts
Bilibili 视频元数据
app/atom/route.ts
生成 Atom Feed
app/opengraph-image/route.tsx
首页 OG 图片
app/(post)/og/[id]/route.tsx
单篇文章 OG 图片
app/about/opengraph-image/route.tsx
About 页 OG 图片

Upstash Redis 用作外部 API 的缓存层:Tweet 和 Bilibili 视频信息在服务端拉取后写入 Redis,请求失败时回退到缓存。

Notion 非官方 APIapp/(page)/psychology-effects/notion.ts)用于读取两个 database:

  • Micro Logs:首页和 /micro-logs 展示的短日志。
  • 心理学效应:/psychology-effects 的实验条目。

这些数据通过 getMicroLogs()getExps() 获取,并叠加 "use cache" + cacheTag 缓存。

外部数据与缓存

Next.js 16 移除了隐式 fetch 缓存,项目已迁移到 Cache Components 模型:

export async function getPosts() {
  "use cache";
  cacheLife("minutes");
  cacheTag("posts");
  // ...
}
数据函数来源缓存策略
getPosts()
index-posts.json
"use cache" + cacheLife("minutes") + cacheTag("posts")
getFeaturedPages()
index-featured.json
"use cache" + cacheTag("featured-pages")
getMicroLogs()
Notion API
"use cache" + cacheTag("micro-logs")
getExps()
Notion API
"use cache" + cacheTag("psychology-effects")
Tweet / Bilibili
外部 API + Redis
服务端拉取,Redis 持久化

客户端不再使用 SWR 轮询或 setInterval 刷新 Notion 数据;更新依赖缓存过期或后续 webhook + revalidateTag 按需失效。

proxy.tsx(Next.js 16 对 middleware 的替代)目前仅注入 x-edge-age 响应头,用于观测边缘存活时间。

构建与运行

包管理使用 pnpm

常用命令:

pnpm dev          # 开发服务器(绑定 0.0.0.0)
pnpm build        # 生产构建
pnpm start        # 生产启动
pnpm new-post     # 交互式创建文章
pnpm format       # Prettier 格式化
pnpm format:check # 格式检查

构建由 next build 完成,默认启用 Turbopack。MDX 页面在构建时编译为 React 组件,OG 图片路由在请求时动态生成(ImageResponse + 本地 woff 字体)。

环境变量(.env.example)主要用于本地脚本:腾讯云翻译 API 密钥(pnpm new-post)和 Upstash Redis Token(Tweet / Bilibili 缓存)。

托管与可观测性

站点托管在 Vercel,域名 jonaszhou.com

  • 客户端分析:@vercel/analytics
  • 性能监控:@vercel/speed-insights
  • Feed:/atom 提供 Atom XML
  • OG:各页面和文章均有动态生成的 Open Graph 图片

为何这样设计

这个架构适合我的使用方式:

  • MDX 适合长期写作,且每篇文章可以是独立的「小型应用」。
  • JSON 索引让首页和 Feed 无需扫描文件系统,维护成本可控。
  • Notion 作为 Micro Logs 和实验数据的编辑入口,写起来比改代码快。
  • Next.js App Router 把内容、组件、API 和 OG 生成放在一个项目里。
  • Cache Components 让静态壳与 Notion 动态数据可以并存,而不需要客户端轮询。
  • Redis 缓存外部 API 结果,避免 Tweet / Bilibili 嵌入拖慢页面或触发限流。

它不是最简单的博客架构——没有直接用 Notion 或 Contentlayer 做全文 CMS,也没有多语言路由——但它把「个人简介、正式文章、轻量日志、交互工具、外部嵌入」放在同一个可维护的 Next.js 项目里,并且保留了向 Cache Components 和按需失效继续演进的空间。