VitePress 知识库工程实践
VitePress 是基于 Vite 的静态站点生成器。上手很容易,难的是内容涨到几百上千篇之后——导航怎么不手工维护、格式怎么不靠自觉、坏链怎么在提交前就拦住。
本文记录本站(900+ 篇 Markdown)的实际做法,是可直接照抄的工程配置,而非入门教程。
🎯 本文的定位
这不是「VitePress 有哪些功能」,而是「这个站是怎么搭的」。所有配置片段都来自本仓库的真实文件,可以直接对照 docs/.vitepress/、tools/、package.json 查看。
一、技术栈与版本约束
{
"engines": { "node": ">=22.x" },
"type": "module",
"dependencies": {
"vitepress": "^1.6.4",
"mermaid": "^11.4.1",
"vitepress-plugin-mermaid": "^2.0.17",
"markdown-it-mathjax3": "^4.3.2",
"markdown-it-task-lists": "^2.1.1"
},
"devDependencies": {
"husky": "^9.1.7",
"tailwindcss": "...", // 仅用于构建独立 HTML 页的静态 CSS
"lucide-static": "..." // 图标 SVG 源,不进运行时
}
}注意 tailwindcss 与 lucide-static 都是 devDependency——它们只在构建期产出静态资源,不会成为站点的运行时依赖。这是「样式不走 CDN」策略的实现方式(见第九节)。
Node 版本用 .npmrc 强制,避免「本地能构建、CI 失败」:
engine-strict=true
legacy-peer-deps=falseengine-strict=true 让 Node 版本不符时 npm install 直接失败而不是警告。这是最省事的环境一致性保障。
二、目录结构
docs/
├── .vitepress/
│ ├── config.js # 站点配置
│ ├── sidebar.generated.js # ⚠️ 生成文件,不要手改
│ └── theme/
│ ├── index.js # 主题扩展入口
│ ├── custom.css # 仅做 @import 汇总
│ ├── styles/ # 按关注点拆分的 9 个样式文件(tokens 打头)
│ └── components/ # HomeSearch.vue / ReferenceMeta.vue
├── study-notes/ # 内容分区一
├── research/ # 内容分区二
├── references/ # 内容分区三
├── pages/ # HTML 精选页的 Markdown 入口
└── public/ # 静态资源 + 独立 HTML 页
tools/ # 生成器与校验脚本
├── lib/ # 共享模块(paths / utils / content)
└── archive/ # 已完成使命的一次性迁移脚本
.husky/pre-commit # 提交前自动化核心原则:内容分区靠目录划分,导航靠脚本生成,样式靠关注点拆分。 三者都不手工维护。
三、内容组织:目录 + _meta.json + frontmatter
这是全站最重要的约定。每个内容目录用一个 _meta.json 描述自己:
{
"label": "🤖 Agent、协议与能力扩展",
"order": 30,
"collapsed": true,
"items": {
"ai-agent-guide": "AI Agent 完全指南",
"mcp-guide": "MCP (Model Context Protocol) 完全指南"
}
}| 字段 | 作用 |
|---|---|
label | 侧边栏显示的分组名(可带 emoji) |
order | 分组排序,数字小的在前 |
collapsed | 分组默认是否折叠 |
items | 显式指定本目录页面的顺序与标题;不列的按默认规则排 |
⚠️ items 只列当前目录
items 不重复收录子目录的内容——子目录由它自己的 _meta.json 负责。层级关系由目录树本身表达,不在 JSON 里重建。
页面级信息走 frontmatter:
---
title: 页面标题
description: 一句话摘要,用于 SEO 与索引
tags: [标签1, 标签2]
author: Hugo
date: 2026-08-14
order: 10 # 目录内排序(与 _meta.json 的 items 二选一)
---四、侧边栏生成器:不手工维护导航
几百篇文档手写 sidebar 配置是不可维护的。本站用 tools/generate-sidebar.js 扫描目录树,结合 _meta.json 和 frontmatter 生成配置:
// config.js —— 只负责导入,不含任何手写的侧边栏结构
import { generatedSidebar } from "./sidebar.generated.js";
themeConfig: {
// 使用自动生成的 sidebar 配置
// 运行 `node tools/generate-sidebar.js` 更新
sidebar: generatedSidebar,
}并用 Git hook 保证它永远不过期:
# .husky/pre-commit
npm run sidebar:generate
git add docs/.vitepress/sidebar.generated.js每次提交自动重新生成并暂存。新增文档时不需要记得更新导航——这是整套设计里收益最高的一环。
⚠️ sidebar.generated.js 是生成物
永远不要手改它。手改会在下次提交时被 hook 覆盖,且改动无迹可寻。要调整导航,改 _meta.json 或生成器逻辑。
调试生成结果可以先干跑:node tools/generate-sidebar.js --dry-run
五、四项自动校验
npm run docs:check 调用 tools/parallel-check.js,并行执行四项检查:
| 校验 | 脚本 | 检查什么 |
|---|---|---|
| 元数据 | check-meta-config.js | _meta.json 的字段合法性、items 是否指向存在的文件 |
| 内容架构 | check-architecture.js | 目录归属、分区边界(如 private/ 未泄漏进 docs/) |
| Markdown 坏链 | check-markdown-links.js | 站内相对链接是否指向真实文件 |
| 格式结构 | check-markdown-format.js | 标题层级、frontmatter 完整性等结构规范 |
关键在于它被编进了构建流程,不是需要记得手动跑的东西:
"docs:build": "npm run docs:build:content && npm run docs:build:vitepress",
"docs:build:content": "npm run docs:check && npm run stats:update && npm run pages:css",
"docs:build:vitepress": "node --max-old-space-size=4096 ./node_modules/vitepress/bin/vitepress.js build docs"校验不过 → 构建不进行 → 部署不会发布。
内容阶段(docs:build:content)做三件事:校验 → 更新统计 → 构建独立页 CSS,全部完成后才进入 VitePress 构建。
💡 为什么要 --max-old-space-size=4096
文档数量上千后,VitePress 构建会触及 Node 默认堆上限而 OOM。显式调高到 4GB 是最直接的解法;仓库里另备了 docs:build:debug(8GB + --trace-uncaught)用于排查构建期崩溃。
六、脚本一览
| 命令 | 用途 |
|---|---|
npm run docs:dev | 本地开发服务器 |
npm run docs:build | 生产构建(含校验与统计更新)——最高强度验证 |
npm run docs:preview | 预览构建产物 |
npm run docs:check | 四项校验(日常改动用它就够) |
npm run sidebar:generate | 重新生成侧边栏 |
npm run pages:css | 为独立 HTML 页构建静态 Tailwind CSS |
npm run palette:generate | 重新生成调色板变量(仅调色板方案变动时) |
npm run new:note | 从模板创建新笔记 |
npm run new:private | 在仓库根 private/ 创建私密文档(不进站点) |
npm run stats:update | 更新内容统计数据 |
npm run refs:update | 批量更新引用链接 |
npm run format:audit | 格式审计(只报告不修改) |
日常节奏:改内容跑 docs:check,改导航或主题跑 docs:build。
⚠️ 独立 HTML 页只能在 docs:preview 里验证
docs/public/pages/ 下的独立页不经过 VitePress 渲染。在 docs:dev 下访问它们不带 .html 的干净 URL 会被 VitePress 的 SPA 路由接管而显示 404——这不是页面坏了。用 npm run docs:preview 验证。
七、config.js 的关键配置
import { defineConfig } from "vitepress";
import taskLists from "markdown-it-task-lists";
import { withMermaid } from "vitepress-plugin-mermaid";
import { generatedSidebar } from "./sidebar.generated.js";
const config = withMermaid( // ← Mermaid 通过包装器注入
defineConfig({
cleanUrls: true, // URL 去掉 .html 后缀
lastUpdated: true, // 展示 Git 最后更新时间
ignoreDeadLinks: true, // 见下方说明
markdown: {
config: (md) => { md.use(taskLists); },
},
themeConfig: { sidebar: generatedSidebar, /* ... */ },
})
);⚠️ 关于 ignoreDeadLinks: true
这看起来是在掩盖问题,实际是为了绕开中文文件名在不同平台的编码差异导致的误报。代价是 VitePress 自带的死链检查失效——所以本站用 tools/check-markdown-links.js 自己实现了一套,在构建前运行。
关掉内置检查的前提是有替代方案,否则就是纯粹的技术债。
7.1 transformPageData:按路径自动打样式类
这是本站主题设计的核心技巧。三大内容分区需要不同的视觉标识,但不希望每篇文档手写 pageClass:
transformPageData(pageData) {
const { relativePath } = pageData;
const isStudyNote = relativePath.startsWith("study-notes/");
const isResearchNote = relativePath.startsWith("research/");
const isReference = relativePath.startsWith("references/");
if (!isStudyNote && !isResearchNote && !isReference) return;
const pageClasses = new Set(/* 已有的 pageClass */);
if (isStudyNote) pageClasses.add("section-study");
if (isResearchNote) pageClasses.add("section-research");
if (isReference) pageClasses.add("section-reference");
// 非落地页统一加 knowledge-doc,承载共享正文样式
if (!pageClasses.has("section-landing")) pageClasses.add("knowledge-doc");
return { frontmatter: { ...pageData.frontmatter,
pageClass: [...pageClasses].join(" ") } };
}收益:新增文档零配置即获得正确样式;调整某个分区的视觉只需改一处 CSS,不用批量改正文文件。
7.2 主题扩展
// theme/index.js
export default {
extends: DefaultTheme,
Layout: () => h(DefaultTheme.Layout, null, {
"doc-before": () => h(ReferenceMeta), // 插槽注入政策元信息组件
}),
enhanceApp({ app }) {
app.component("HomeSearch", HomeSearch);
app.component("Mermaid", Mermaid); // 异步加载,避免拖慢首屏
},
};用 extends + 插槽扩展默认主题,而不是 fork 一套主题——升级 VitePress 时几乎无迁移成本。
7.3 样式分层
custom.css 只做汇总,不含任何具体规则:
/* 模块化样式入口。tokens 必须最先加载,其余文件只引用其中的变量。 */
@import './styles/tokens.css';
@import './styles/mermaid.css';
@import './styles/mathjax.css';
@import './styles/doc-enhance.css';
@import './styles/knowledge-content.css';
@import './styles/certificates.css';
@import './styles/home.css';
@import './styles/section-landing.css';
@import './styles/reference-meta.css';按关注点拆分而非按页面拆分。改 Mermaid 呈现只动 mermaid.css,不会波及别处。
顺序有一处是硬性的:tokens.css 必须最先加载——它定义全站设计令牌(颜色、间距等变量),其余文件只引用不重复定义。这样换配色只改一个文件,不用在九个文件里搜色值。
八、Markdown 能力
8.1 容器
::: tip 提示
内容
:::
::: warning 警告
内容
:::
::: danger 危险
内容
:::
::: details 点击展开
折叠内容
:::💡 使用原则
核心结论与推理必须直接可见;只有辅助解释、长示例、补充材料才折叠。把关键结论藏进 details 是常见的反模式。
8.2 代码组
console.log("Hello");print("Hello")8.3 Mermaid 图表
通过 withMermaid 包装器启用,直接写围栏即可:
```mermaid
flowchart LR
A["源文件"] --> B["校验"] --> C["构建"] --> D["部署"]
```8.4 数学公式
markdown-it-mathjax3 提供支持:行内 $E = mc^2$,块级用 $$ ... $$。
8.5 任务列表
markdown-it-task-lists 提供:
- 已完成项
- 未完成项
九、独立 HTML 页:第二条样式管线
站点里还有一类内容不经过 VitePress——docs/public/pages/ 下的独立 HTML 页。它们自成一套资源体系,集中在 _shared/:
| 文件 | 职责 | 性质 |
|---|---|---|
palette.css | Tailwind 调色板的 CSS 变量,深色模式下色阶反转 | 生成物 |
tailwind.css | 静态 Tailwind 工具类,扫描各页 HTML 生成 | 生成物 |
page-shell.css | 页面骨架、--page-* 设计令牌、字体 @font-face | 手写 |
page-common.css | 公共交互样式(入场动画、卡片悬停、图表容器、滚动条) | 手写 |
fonts/ | 自托管可变字体 | 资源 |
核心约定是一条取舍:
💡 样式不走 CDN,图表库继续走 CDN
- Tailwind 与图标已本地化——它们决定排版,CDN 不可达时整页会散架,代价不可接受。
- ECharts / Chart.js 保留 CDN——体积大(MB 级)且只有少数页面用到,加载失败的后果仅是图表区空白,页面结构完好。为此把二进制塞进仓库不划算。
判断标准不是「CDN 好不好」,而是「这个依赖失败时,页面是散架还是局部降级」。
其余几条约定:
- 不写运行时 Tailwind 配置。颜色一律用标准工具类(
bg-slate-800、text-blue-600),它们已接到色阶变量上,深色模式自动跟随。 - 图标用内联 SVG,从
lucide-static取源,不引入图标字体或运行时图标库。 text-white与bg-white语义不同:white在调色板里保持字面值,所以text-white在深色下仍是白色(有色按钮需要);卡片底色bg-white由page-shell.css单独改写。- 新增页面后必须跑
npm run pages:css,否则新用到的工具类不会出现在产出的 CSS 里。
十、部署:Vercel
{
"buildCommand": "npm run docs:build",
"outputDirectory": "docs/.vitepress/dist",
"cleanUrls": true,
"redirects": [
{ "source": "/旧路径", "destination": "/新路径", "permanent": true }
],
"headers": [
{ "source": "/assets/(.*)",
"headers": [{ "key": "Cache-Control",
"value": "public, max-age=31536000, immutable" }] },
{ "source": "/images/(.*)",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=604800" }] }
]
}两个值得抄的做法:
- 缓存策略分级。
/assets/是构建产物、文件名带哈希,可以immutable缓存一年;/images/是手工维护的静态图,路径固定,只缓存 7 天以便替换后能较快生效。 - 内容搬家配
redirects。文档重组后老链接会散落在收藏夹和外部引用里,permanent: true(301)既保住入口也保住 SEO 权重。
十一、常见问题排查
| 现象 | 原因 | 处理 |
|---|---|---|
npm install 直接失败 | Node 版本 < 22,被 engine-strict 拦住 | node -v 确认后升级,再重装 |
| 侧边栏与实际目录不符 | 手改过生成文件,或没跑生成器 | npm run sidebar:generate 后检查暂存区 |
| 构建 OOM | 文档量超出 Node 默认堆 | 已配 --max-old-space-size=4096,不够就用 docs:build:debug |
| 站内链接 404 | 内置死链检查已关闭 | 跑 npm run links:check |
| 图片不显示 | 相对路径在不同层级失效 | 站点根路径写法(/images/x.png),并在 dev 下点验 |
| 中文文件名跨平台异常 | 编码差异 | 优先 kebab-case.md;必须用中文时在 Windows/Linux 都验证一遍 |
十二、可迁移的经验
抛开本站的具体实现,这套配置里真正通用的是四条:
- 导航必须生成,不能手写。 一旦文档数量超过几十篇,手写侧边栏就会开始落后于现实。配上 pre-commit 让它无法过期。
- 校验要编进构建,不能靠自觉。 「记得跑一下检查」在个人项目里必然失效;
docs:build依赖docs:check才是可靠的。 - 关掉内置检查就要自己补上。
ignoreDeadLinks: true若无替代方案,就是把问题推给未来。 - 样式按关注点拆分,用
transformPageData自动挂类。 让正文文件不承载任何呈现信息——这是内容与呈现解耦的关键。
📚 参考资源
| 资源 | 说明 |
|---|---|
| VitePress 官方文档 | 配置项权威参考 |
| VitePress 配置参考 | transformPageData 等钩子说明 |
| 默认主题配置 | 导航、侧边栏、搜索 |
| 主题扩展指南 | 插槽与 enhanceApp |
| vitepress-plugin-mermaid | Mermaid 集成 |
| Vercel 配置参考 | vercel.json 全字段 |
| Husky 文档 | Git hooks |