Skip to content

VitePress 主题升级踩坑记:六个报错与根因修复 ​

博客上线后做了一次依赖升级,把 @sugarat/theme 从 0.4.10 提升到 0.5.27,vitepress 从 1.6.0 提升到 1.6.4。

升级命令只有一行:

bash
npm install -D @sugarat/theme@latest vitepress@latest

但接下来连续踩了六个坑 —— 性质完全不同:编译期一个、构建期一个、网络层一个、开发服务器一个、路由一个、静态资源一个。每个坑都记下原始报错、根因定位与最终修复。

坑一:Vue SFC 宏类型解析失败(编译期) ​

启动开发服务器后,终端刷出大量报错:

text
[vitepress] Internal server error: [@vue/compiler-sfc] Failed to load TypeScript,
which is required for resolving imported types.
Please make sure "TypeScript" is installed as a project dependency.

.../node_modules/vitepress-plugin-pagefind/src/command-palette/CommandRoot.vue

根因 ​

0.5.27 版本默认启用了主题自带的 vitepress-plugin-pagefind 离线搜索插件。该插件的 Vue 组件里用了跨文件导入的泛型宏:

vue
<script setup lang="ts">
import type { CommandRootProps } from './types'

const props = withDefaults(defineProps<CommandRootProps>(), {
  theme: 'default',
})
</script>

在 Windows 的 Vite 虚拟模块环境下,编译器无法通过 fs 去跨文件解析这个类型定义,于是直接抛错。

修复 ​

项目里已经配置了 VitePress 官方原生的本地全文搜索(MiniSearch 实现,毫秒响应、无外部依赖),和这个第三方插件功能重复,直接关掉插件:

typescript
const blogTheme = getThemeConfig({
  author: '林涛',
  // 禁用主题内置的 pagefind 搜索插件,改用 VitePress 官方原生搜索
  search: false,
  // ...其余配置
})

坑二:SSR 阶段无法识别 .vue 后缀(构建期) ​

本地 npm run docs:dev 正常,但 npm run docs:build 直接中断:

text
build error:
Unknown file extension ".vue" for
node_modules/vitepress-plugin-product-card/dist/components/ProductCard.vue
TypeError [ERR_UNKNOWN_FILE_EXTENSION]
    at Object.getFileProtocolModuleFormat [as file:]

根因 ​

新版主题引入了 vitepress-plugin-product-card,而它的入口文件直接 import 了 .vue 源文件:

javascript
// node_modules/vitepress-plugin-product-card/dist/client.mjs
import ProductCard from "./components/ProductCard.vue";

VitePress 在生产构建时,默认会把 node_modules 里的依赖「外部化」—— 不打包,直接交给 Node 原生 ESM 加载器去 import。而 Node 原生加载器只认 .js / .mjs / .json,不认识 .vue 后缀,于是构建直接失败。

这也是为什么开发环境看不出来:dev 模式下 Vite 会全量处理依赖,只有生产构建才会走外部化逻辑。

修复 ​

在 VitePress 配置里强制这些包参与 Vite 打包,让 Vue 插件能正常编译其中的 .vue 文件:

typescript
export default defineConfig({
  // ...其余配置
  vite: {
    ssr: {
      noExternal: ['@sugarat/theme', 'vitepress-plugin-product-card']
    }
  }
})

改完后重新构建:

text
✓ building client + server bundles...
✓ rendering pages...
build complete in 10.33s.

坑三:GitHub 连接被重置(网络层) ​

代码改好了,git push 却推不上去:

text
fatal: unable to access 'https://github.com/.../lintao-blog.git/':
Recv failure: Connection was reset

重试几次后恶化为连接超时:

text
Failed to connect to github.com:443 after 21083 ms: Could not connect to server

第一次定位:以为只是 DNS 污染(治标) ​

按下面的顺序逐层排查:

排查项结果
Resolve-DnsName github.com解析到 20.205.243.166
TCP 连接 20.205.243.166:443超时被阻断
TCP 连接 140.82.112.3:443正常
TCP 连接 140.82.113.3:443正常
访问 gitee / npm 镜像正常

看起来是 DNS 把 github.com 解析到了一个被阻断的节点,于是在 hosts 里把它固定到可达节点:

text
# C:\Windows\System32\drivers\etc\hosts
140.82.112.3    github.com
bash
ipconfig /flushdns

git push 果然恢复了 —— 但没过多久又失败,说明 hosts 只是治标。

第二次定位:真正的根因是 Git 不读系统代理 ​

本机开着 Clash 一类的代理软件,Windows 系统代理指向 127.0.0.1:7897。浏览器走系统代理,访问 GitHub 一切正常;但 Git 并不读取 Windows 的系统代理设置,它一直裸连 github.com,裸连被阻断才超时。

让 curl 打印连接细节即可验证:

bash
GIT_CURL_VERBOSE=1 git ls-remote origin

输出里出现:

text
* Trying 127.0.0.1:7897...
* CONNECT tunnel established, response 200

说明这次终于走了代理,链路是通的。

修复 ​

给 Git 单独指定代理:

bash
git config --global http.proxy http://127.0.0.1:7897
git config --global https.proxy http://127.0.0.1:7897

同时把 hosts 里那条临时记录删掉,恢复干净状态。

注意:这个方案依赖代理软件常开。代理关掉后 push 会再次超时,此时可以临时撤销:

bash
git config --global --unset http.proxy
git config --global --unset https.proxy

坑四:开发服务器启动即崩溃(开发服务器) ​

npm run docs:dev 启动后立刻退出:

text
Error: ENOENT: no such file or directory, open
'D:\WorkBuddy工作区\vitepress博客\docs\data\posts.data.mts.timestamp-1789185175132-b0f18c33a5afc.mjs'
    at @sugarat/theme/node.mjs:504

根因 ​

链条有点长,读主题源码后理清了:

  1. @sugarat/theme 的热重载插件注册了 server.watcher 的 add / change / unlink 监听,完全没有过滤文件扩展名(对比:VitePress 自身的处理器有 file.endsWith('.md') 保护)。
  2. VitePress 编译 .data.mts 内容加载器时,Vite 会在同级目录生成一个 ${fileName}.timestamp-${Date.now()}-${rand}.mjs 临时文件,import 完立即删除。
  3. 临时文件被删除后仍有一次 change 事件进入主题回调 → 主题去 readFile 这个已删除的文件 → ENOENT 未捕获 → 整个 dev 进程退出。

因为本质是「删除」与「读取」的竞态,所以它表现为偶发崩溃,而不是每次必现。

修复 ​

把临时文件和加载器目录排除出文件监听:

typescript
export default defineConfig({
  vite: {
    server: {
      watch: {
        // 排除 Vite 的 .data.mts 临时文件与加载器目录,
        // 避免主题 watcher 读到已删除的文件导致 dev 崩溃
        ignored: ['**/*.timestamp-*', '**/docs/data/**']
      }
    }
  }
})

Vite 会把这里的 ignored 追加到默认忽略列表(**/.git/**、**/node_modules/** 等),不会覆盖,所以没有副作用。

坑五:导航栏链接全部 404(路由 / 内容) ​

导航栏配了「技术与软件 / 一人公司 / 归档」三个链接,点进去全是 404 —— 因为当时根本没有对应的页面文件。

根因 ​

主题并不自带分类页和归档页:

  • src/components/TimelinePage.vue 只是个占位组件(内容就四个字「时间线页面」),归档页要自己实现;
  • 主题自带的 BlogList.vue 依赖首页写入的全局 useActiveTag() 状态,在独立页面里复用不可靠。

同时发现一个容易踩的坑:article: false 在 0.5.27 里是无效字段。主题只认 publish: false(它会把文章置为 hidden: true 并关闭推荐)。about.md 原本写的是 article: false,所以「关于我」被当成普通文章显示在了首页列表里。

修复 ​

用 VitePress 官方的 createContentLoader 自己做内容加载,再写一个可复用列表组件:

typescript
// docs/data/posts.data.mts
import { createContentLoader } from 'vitepress'

export default createContentLoader('posts/*.md', {
  excerpt: true,
  transform(raw) {
    return raw
      .filter(page => page.frontmatter.layout !== 'home' && page.frontmatter.publish !== false)
      .map(page => ({
        title: page.frontmatter.title,
        url: page.url,
        date: page.frontmatter.date,
        categories: page.frontmatter.categories ?? [],
        tags: page.frontmatter.tags ?? [],
        description: page.frontmatter.description ?? ''
      }))
      .sort((a, b) => +new Date(b.date) - +new Date(a.date))
  }
})

新增文件:

  • docs/data/posts.data.mts —— 统一内容加载器;
  • docs/.vitepress/theme/BlogPostList.vue —— 可复用列表(卡片模式 / grouped 按年份归档 / 空状态);
  • docs/tech/index.md、docs/opc/index.md —— 按 categories 筛选的分类页;
  • docs/timeline.md —— 按年份分组的归档页。

这几个页面都用 publish: false 排除出文章流,页面内直接 import 组件与加载器,不改动主题入口文件。同时把 about.md 的 article: false 改成 publish: false。

坑六:头像不显示、logo.png 每次构建后消失(静态资源) ​

首页头像一直是空的。为了让它显示,把 logo.png 放进了 docs/.vitepress/dist/ —— 结果每次 docs:build 之后文件就没了。

根因 ​

两个问题叠在一起:

  1. 配置字段是错的。一开始在主题配置里写了 authorAvatar,但翻遍 @sugarat/theme 源码根本没有这个字段,配置从未生效。
  2. 文件放错了目录。dist/ 是构建产物目录,每次构建都会被清空,而且它还被 .gitignore 忽略,放进去既留不住也不会被提交。

主题真正的头像取值链(见 BlogHomeHeaderAvatar.vue / BlogAuthor.vue):

text
frontmatter.logo
  → frontmatter.blog.logo
  → blog.home.logo
  → site.themeConfig.logo
  → 默认 '/logo.png'

最后那个默认回退值就是 /logo.png —— 也就是说,只要文件出现在站点的 /logo.png 路径下,零配置也能生效。

修复 ​

把图片放进 VitePress 的静态资源目录 docs/public/:

text
docs/public/logo.png   →   构建后    dist/logo.png   →   线上   /logo.png

并在首页 frontmatter 里显式声明,方便以后查阅:

yaml
# docs/index.md
blog:
  name: 林涛的博客
  motto: 知其不可奈何而安之若命
  logo: /logo.png

同时删掉配置里那个无效的 authorAvatar。

记住一条判断标准 ​

  • docs/public/ 是静态资源目录,构建时被原样复制到站点根目录,文件名即 URL 路径 —— 图片、图标、音乐都放这里;
  • dist/ 是构建产物,重建即清空 —— 只有构建流程该往里面写东西,人不要手动放。

小结 ​

六个坑分散在不同的层:

#报错层次根因修复
1Failed to load TypeScript编译期pagefind 插件跨文件解析类型宏search: false
2Unknown file extension ".vue"构建期依赖被外部化,Node 不认 .vuevite.ssr.noExternal
3Connection was reset网络层Git 不读 Windows 系统代理git config http.proxy
4ENOENT ... timestamp-*.mjs开发服务器主题 watcher 未过滤临时文件vite.server.watch.ignored
5导航链接 404路由 / 内容页面缺失 + article: false 无效createContentLoader + publish: false
6头像 404 / logo 消失静态资源资源放错目录 + 无效字段docs/public/ + logo 取值链

有两个反复被验证的经验:

  • 报错发生在哪一层,决定排查方向。 编译期看依赖与插件,构建期看外部化策略与打包配置,网络层先怀疑代理和 DNS,资源类问题先确认「路径对不对、目录会不会被清空」。
  • 「本地 dev 正常」不等于「能构建成功」,更不等于「线上正常」。 每次改完配置或升级依赖,跑一遍 npm run docs:build,再 push 交给 Cloudflare 自动部署 —— 大量问题能在推送前就被拦下来。

基于 VitePress & @sugarat/theme 构建