这个网站的技术解析
@jonaszhou|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.mdx、app/projects/page.mdx | — | 顶层说明页 |
notion pages | Notion database | 代码内硬编码 pageId | Micro Logs、心理学效应 |
每篇文章是一个独立目录,内含 page.mdx,可选 components.tsx 或 notion.ts。这意味着单篇文章可以加载任意模块、拥有自定义布局,并利用 Next.js 自动代码分割——单篇的冗余不会拖慢全站。
索引文件维护文章和推荐页的元数据(id、date、title、url、order)。页面通过 getPosts() 和 getFeaturedPages() 读取索引,而不是在构建时扫描文件系统。
新建文章可使用 pnpm new-post 脚本:交互式输入标题和描述,自动生成 MDX 骨架,并将条目写入索引(中文标题可经腾讯云翻译 API 转为英文 slug)。
Markdown 管线
Markdown 构建管线基于 @next/mdx,配置在 next.config.js:
pageExtensions包含md和mdx,MDX 文件可直接作为页面。experimental.mdxRs: true:启用 Rust 版 MDX 编译器,加快编译速度。cacheComponents: true:启用 Next.js 16 Cache Components 模型。
mdx-components.ts 将 MDX 元素映射到自定义组件,构成站点的样式指南:
| 类别 | 组件 |
|---|---|
排版 | H1 / H2 / H3、P、A、OL / UL / LI、Blockquote、HR |
代码 | Code、Snippet(<pre> 替换) |
媒体 | Image、Figure、Caption |
嵌入 | Tweet、YouTube、Bilibili、Mermaid |
辅助 | Callout、DataTable、脚注(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.js和scripts/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 非官方 API(app/(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 和按需失效继续演进的空间。