跳转到正文

VitePress 知识库工程实践

VitePress 是基于 Vite 的静态站点生成器。上手很容易,难的是内容涨到几百上千篇之后——导航怎么不手工维护、格式怎么不靠自觉、坏链怎么在提交前就拦住

本文记录本站(900+ 篇 Markdown)的实际做法,是可直接照抄的工程配置,而非入门教程。

🎯 本文的定位

这不是「VitePress 有哪些功能」,而是「这个站是怎么搭的」。所有配置片段都来自本仓库的真实文件,可以直接对照 docs/.vitepress/tools/package.json 查看。

一、技术栈与版本约束

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 源,不进运行时
  }
}

注意 tailwindcsslucide-static 都是 devDependency——它们只在构建期产出静态资源,不会成为站点的运行时依赖。这是「样式不走 CDN」策略的实现方式(见第九节)。

Node 版本用 .npmrc 强制,避免「本地能构建、CI 失败」:

ini
engine-strict=true
legacy-peer-deps=false

engine-strict=true 让 Node 版本不符时 npm install 直接失败而不是警告。这是最省事的环境一致性保障。


二、目录结构

text
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 描述自己:

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:

yaml
---
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 生成配置:

javascript
// config.js —— 只负责导入,不含任何手写的侧边栏结构
import { generatedSidebar } from "./sidebar.generated.js";

themeConfig: {
  // 使用自动生成的 sidebar 配置
  // 运行 `node tools/generate-sidebar.js` 更新
  sidebar: generatedSidebar,
}

并用 Git hook 保证它永远不过期

sh
# .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 完整性等结构规范

关键在于它被编进了构建流程,不是需要记得手动跑的东西:

json
"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 的关键配置

javascript
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

javascript
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 主题扩展

javascript
// 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 只做汇总,不含任何具体规则:

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 容器

markdown
::: tip 提示
内容
:::

::: warning 警告
内容
:::

::: danger 危险
内容
:::

::: details 点击展开
折叠内容
:::

💡 使用原则

核心结论与推理必须直接可见;只有辅助解释、长示例、补充材料才折叠。把关键结论藏进 details 是常见的反模式。

8.2 代码组

javascript
console.log("Hello");
python
print("Hello")

8.3 Mermaid 图表

通过 withMermaid 包装器启用,直接写围栏即可:

markdown
```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.cssTailwind 调色板的 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-800text-blue-600),它们已接到色阶变量上,深色模式自动跟随。
  • 图标用内联 SVG,从 lucide-static 取源,不引入图标字体或运行时图标库。
  • text-whitebg-white 语义不同white 在调色板里保持字面值,所以 text-white 在深色下仍是白色(有色按钮需要);卡片底色 bg-whitepage-shell.css 单独改写。
  • 新增页面后必须跑 npm run pages:css,否则新用到的工具类不会出现在产出的 CSS 里。

十、部署:Vercel

json
{
  "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" }] }
  ]
}

两个值得抄的做法:

  1. 缓存策略分级/assets/ 是构建产物、文件名带哈希,可以 immutable 缓存一年;/images/ 是手工维护的静态图,路径固定,只缓存 7 天以便替换后能较快生效。
  2. 内容搬家配 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 都验证一遍

十二、可迁移的经验

抛开本站的具体实现,这套配置里真正通用的是四条:

  1. 导航必须生成,不能手写。 一旦文档数量超过几十篇,手写侧边栏就会开始落后于现实。配上 pre-commit 让它无法过期。
  2. 校验要编进构建,不能靠自觉。 「记得跑一下检查」在个人项目里必然失效;docs:build 依赖 docs:check 才是可靠的。
  3. 关掉内置检查就要自己补上。 ignoreDeadLinks: true 若无替代方案,就是把问题推给未来。
  4. 样式按关注点拆分,用 transformPageData 自动挂类。 让正文文件不承载任何呈现信息——这是内容与呈现解耦的关键。

📚 参考资源

资源说明
VitePress 官方文档配置项权威参考
VitePress 配置参考transformPageData 等钩子说明
默认主题配置导航、侧边栏、搜索
主题扩展指南插槽与 enhanceApp
vitepress-plugin-mermaidMermaid 集成
Vercel 配置参考vercel.json 全字段
Husky 文档Git hooks

← 返回 开发环境与基础工具