项目结构、文件位置与 URL 规则
这一篇解决最基础也最容易出错的问题:文件到底该放哪里,浏览器里的 URL 又是由什么决定的。你不需要先理解 VitePress 的全部原理,但必须分清「我编辑的源文件」和「工具生成的文件」,否则很容易改错位置,或者把构建产物提交进仓库。
本站的 VitePress 源目录是 docs。docs/.vitepress/config.ts 中设置了 rewrites: createRewrites({ srcDir: "docs" })、cleanUrls: true、lastUpdated: true 和 sitemap,主题的 permalink 插件也处于启用状态。实际公开地址以页面 frontmatter 的 permalink 为主,文件路径只是源文件位置,不应靠猜 URL。
先认识源文件和生成文件
| 类型 | 常见路径 | 是否手改 | 为什么 |
|---|---|---|---|
| 文章源文件 | docs/posts/**/*.md | 可以 | 你的正文和文章 frontmatter,是内容的事实来源 |
| 目录页源文件 | docs/catalog/**/*.md | 可以 | 提供目录页 frontmatter 和 path 扫描入口 |
| 静态资源源文件 | docs/public/** | 可以 | 会原样复制到站点根路径,例如 /home/bg18.webp |
| 配置文件 | docs/.vitepress/config.ts、sidebar.ts、search-manifest.ts、Nav.ts、Features.ts | 谨慎修改 | 控制路由、侧边栏、搜索清单、导航和首页卡片 |
| 构建产物 | docs/.vitepress/dist/ | 不可以 | pnpm docs:build 生成,部署也从这里取文件 |
| 开发缓存 | docs/.vitepress/cache/ | 不可以 | Vite/VitePress 自动维护,删除后也会重建 |
| 搜索清单 | docs/public/search-manifest.json | 不可以 | 启动时由 search-manifest.ts 扫描 docs/posts 自动写出 |
| 包体分析文件 | stats.html | 不可以 | 构建分析工具生成,.gitignore 已忽略 |
生成文件为什么会出现在源码目录里?因为工具有时会把结果放到公共目录或 .vitepress 旁边。例如 docs/public/search-manifest.json 看起来像普通静态文件,实际上是 plugins.ts 在加载时调用 getSearchManifest() 并 writeFileSync 生成的;它被 .gitignore 忽略,下一次启动又会重写。另一些工具把 Pagefind 索引、RSS、sitemap 放进 docs/.vitepress/dist/。记住一个原则:能被构建命令重新生成的文件,不作为手写源文件修改。
与本指南相关的完整目录树
E:\vitepress-teek-my-blog\teek-hyde\
├─ docs\ # VitePress 的 srcDir
│ ├─ catalog\ # 目录页,只负责生成文章目录入口
│ │ ├─ 技术笔记.md # /catalog/技术笔记
│ │ ├─ 技术笔记\
│ │ │ └─ 项目记录.md # /catalog/技术笔记/项目记录
│ │ ├─ 生活随笔.md # /catalog/生活随笔
│ │ └─ 算法竞赛.md # /catalog/算法竞赛
│ ├─ posts\ # 正式文章源文件
│ │ ├─ 技术笔记\
│ │ │ ├─ 项目复盘模板.md # /posts/技术笔记/项目复盘模板
│ │ │ └─ 项目记录\
│ │ │ └─ .gitkeep # 空目录占位,不是文章
│ │ ├─ 生活随笔\
│ │ │ └─ .gitkeep
│ │ └─ 算法竞赛\
│ │ └─ .gitkeep
│ ├─ public\ # 静态资源根路径 /
│ │ ├─ home\bg18.webp # 封面常用图
│ │ ├─ images\posts\<slug>\ # 文章正文图片和自定义封面
│ │ └─ search-manifest.json # 自动生成,不手改
│ ├─ writing-guide\ # 写作指南说明页
│ └─ .vitepress\
│ ├─ config.ts # 全局路由、SEO、markdown、sitemap 等
│ ├─ sidebar.ts # 扫描 posts 生成文章侧边栏
│ ├─ search-manifest.ts # 扫描 posts 生成搜索清单元数据
│ ├─ plugins.ts # Pagefind、RSS、搜索清单写入等
│ ├─ ConfigHyde\Nav.ts # 手工维护的顶部导航
│ ├─ ConfigHyde\Features.ts # 手工维护的首页 Banner 功能卡片
│ ├─ dist\ # 构建产物,禁止手改
│ └─ cache\ # 开发缓存,禁止手改
├─ scripts\audit-site.mjs # pnpm docs:audit 使用的 SEO 审计脚本
├─ .cnb.yml # CNB 安装、构建、审计、部署流程
├─ package.json # pnpm 脚本定义
└─ stats.html # 可能生成的包体分析文件,已忽略.gitkeep 不是 VitePress 的特殊文章格式,而是 Git 的约定:Git 不跟踪空目录,所以先放一个空文件占位。扫描器只处理 .md,不会把 .gitkeep 当成文章。.vitepress 下以点开头的目录、posts 下以下划线或点开头的条目会被扫描器跳过,因此不要用 _draft 之类的名字指望它自动变成公开文章。
docs/catalog 与 docs/posts 的分工
| 项目 | docs/catalog/ | docs/posts/ |
|---|---|---|
| 角色 | 目录入口页 | 文章正文页 |
| 典型文件 | docs/catalog/技术笔记.md | docs/posts/技术笔记/项目复盘模板.md |
| 页面靠什么生成 | layout: page + catalogue: true + path | Markdown 正文和文章 frontmatter |
| 是否通常有正文 | 不需要,通常只保留 frontmatter | 需要 H1 和正文 |
| 是否进入目录扫描 | 不被 search-manifest.ts 扫描 | 被侧边栏、搜索清单和文章系统扫描 |
| 是否进入首页/分类/标签/归档 | 不进入 | article: true 时进入 |
| 是否显示评论 | 固定关闭 | 由 comment: true 开启 |
| 是否参与 Pagefind | 固定关闭 | 由 pagefind-indexed: true 开启 |
目录页的 path 指向 docs/posts/ 下的真实文章目录,例如:
path: posts/技术笔记/项目记录这个值不带开头的 /,也不包含 docs/。它相当于从 docs/ 开始的相对路径。目录页的 permalink 则通常镜像为:
permalink: /catalog/技术笔记/项目记录文章目录中为什么不能放 index.md?原因很具体:sidebar.ts 会跳过 name.toLowerCase() === "index.md" 的文件,search-manifest.ts 也会过滤掉 index.md。即使你写了一个 index.md,它也可能不出现在侧边栏和自定义搜索清单里,还会和目录路由产生歧义。正确做法是给每篇文章一个有意义的文件名,目录入口放到 docs/catalog/ 下。
URL 规则:permalink 才是真正的公开地址
本站启用了 cleanUrls: true,所以页面在浏览器里不显示 .html。你写站内链接时也不要加 .html。例如应该写 /writing-guide/new-article,不要写 /writing-guide/new-article.html。
| 内容 | 源文件示例 | 推荐 permalink | 最终访问地址 |
|---|---|---|---|
| 一级目录页 | docs/catalog/技术笔记.md | /catalog/技术笔记 | /catalog/技术笔记 |
| 二级目录页 | docs/catalog/技术笔记/项目记录.md | /catalog/技术笔记/项目记录 | /catalog/技术笔记/项目记录 |
| 一级文章 | docs/posts/技术笔记/01.git-basics.md | /posts/技术笔记/git-basics | /posts/技术笔记/git-basics |
| 二级文章 | docs/posts/技术笔记/项目记录/01.vitepress-deploy-guide.md | /posts/技术笔记/项目记录/vitepress-deploy-guide | /posts/技术笔记/项目记录/vitepress-deploy-guide |
中文 URL 在地址栏中可能显示为百分号编码,例如 /catalog/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0,这是正常现象。Markdown 链接和 frontmatter 中直接写中文即可,例如:
[技术笔记目录](/catalog/技术笔记)显式 frontmatter 建议始终包含:
---
title: VitePress 部署指南
description: 从构建到 CNB 部署的可复现流程。
permalink: /posts/技术笔记/项目记录/vitepress-deploy-guide
categories:
- 技术笔记
tags:
- VitePress
- 部署
article: true
sidebar: true
pagefind-indexed: true
comment: true
---为什么必须显式写 permalink
当前虽然启用了 autoFrontmatter: true,但配置里的 exclude: { title: true, date: true } 会让插件跳过已有普通 frontmatter 的文章;因此不要依赖它给文章补 permalink、分类或封面。date 由单独的 article-date.ts 初始化器在文件首次 Git commit 后,于启动开发服务器或构建时自动写入;读取的是第一次 commit 的作者时间,不是 push 或构建时间,不需要手填。省略 permalink 时,路由可能按 VitePress 源文件路径推导,但这不是稳定、可读的公开 URL,仍必须显式写 /posts/...。
所以流程必须是:先写显式 permalink,再启动开发服务器。 不要先空着看看页面能不能打开。 另外,sidebar.ts 和 search-manifest.ts 在缺少 permalink 时各自有一个回退逻辑:大致按 /posts/<一级分类>/<去掉编号的文件名> 生成链接。这个回退不会可靠地保留多层子目录,所以它只能算兜底,不能作为正式规则。尤其是 项目记录 这类二级目录文章,更必须在 frontmatter 中显式写完整 permalink。 permalink 还应当满足:
- 以
/catalog/或/posts/开头,与内容类型一致; - 全站唯一,不与已有文章、目录页或功能页重复;
- 发布后保持稳定;如果必须改,要同步更新所有站内链接,并在托管平台做旧地址重定向;
- 不包含
.html、空格、反斜杠或结尾斜杠;统一使用/; - 文件路径可以调整,但公开 permalink 不要无故跟着改。
rewrites 和 permalink 插件的存在意味着:源文件相对路径、VitePress 内部路由和最终公开 URL 不是一回事。你可以把「源文件路径」理解为仓库里的存放位置,把 permalink 理解为公开承诺。新增、移动或重命名文件后,不要凭文件名猜浏览器地址,打开页面以 frontmatter 的 permalink 为准。
文件和目录命名规则
| 对象 | 推荐写法 | 说明 |
|---|---|---|
| 一级分类目录 | docs/posts/技术笔记/、docs/posts/生活随笔/ | 目录名就是分类的物理来源,categories 应与其保持一致 |
| 文章子目录 | docs/posts/技术笔记/项目记录/ | 与 docs/catalog/技术笔记/项目记录.md 镜像;不要在 posts 放 index.md |
| 文章文件名 | 01.vitepress-deploy-guide.md | 两位编号 + 点 + 英文 slug + .md |
| 文章 H1 / title | VitePress 部署指南 | 可以是中文,且 H1 与 title 保持一致 |
| 图片目录 | docs/public/images/posts/vitepress-deploy-guide/ | 与文章 slug 对应,便于查找和迁移 |
| 正文图片引用 | /images/posts/vitepress-deploy-guide/build-success.webp | 对应 docs/public 下的资源,不要写 docs/public/... |
命名的重要约束:
- 文件名和目录名不要包含空格,不要包含
/、\、:、*、?、<、>、|等路径或 Windows 非法字符。 - 文章文件名推荐 ASCII slug,避免大小写混用;URL 使用小写和连字符更稳定。文件和目录名可以用中文,但要保持全站一致,不要今天写
技术笔记、明天写技术笔记(末尾空格)。 NN.前缀用于排序。sidebar.ts和search-manifest.ts都会读取^(\d+)\.,并在部分回退链接中去掉前缀。它不决定首页时间排序。- 不要在
posts目录中使用index.md;不要用.或_开头的条目来「临时发布」,这些条目会被扫描器跳过。 - 发布后不要为了整理好看而随意改文件名。即使显式 permalink 保住了 URL,文件重命名还是会影响排序、图片目录、Git 历史和后续维护。
如果你确实要重命名或移动,优先使用 git mv 保留历史,并把它当作一次需要重新验收的结构变更,而不是普通保存。
文章图片和封面路径
正文图片的真实文件放在:
docs/public/images/posts/vitepress-deploy-guide/build-success.webp在 Markdown 中引用时省略 docs/public,因为 docs/public 的内容会映射到站点根路径:
规则说明:
- 图片必须放在
docs/public/下,且引用路径以/开头;写成相对路径或写成/public/...都会导致 404。 - 每张正文图片都必须有 alt。审计只检查 alt 属性是否存在,空 alt 也可能通过,所以更重要的是写出能描述图片内容的实质文字,帮助屏幕阅读器;不要写没有信息量的占位文字。
- 推荐使用
.webp等浏览器友好的格式;上传原图前先压缩,避免首页和文章加载过慢。 coverImg也使用站点根路径。需要固定首页卡片或固定分享图时,可写/images/posts/<slug>/cover.webp或仓库已有/home/bgNN.webp。- 不写
coverImg时,首页文章卡片会先随机使用正文中可识别的图片;正文也没有图片时,从/home/bg*.webp随机选择。只有希望封面固定、或希望社交分享图稳定时才必须写coverImg。 - 首页随机封面不会改变 SEO 分享图;不写
coverImg时,OG/Twitter 分享图仍然使用站点默认图。正文图片请使用/images/...这种站内绝对路径,相对路径不会稳定进入候选列表。
排序:文件名编号、sidebarSort、date、sticky 和 top 的区别
这些排序信号作用在不同页面,不能互相替代。理解它们的最小记忆是:侧边栏看编号或 sidebarSort,首页和时间线看 date,置顶看 sticky,精选卡片看 top。
| 机制 | 写在哪里 | 主要影响 | 规则和注意点 |
|---|---|---|---|
| 文件名数字前缀 | 文章文件或子目录名 | 侧边栏、搜索清单的目录/文件顺序 | 形如 01.slug.md 或 10.子目录;sidebar.ts 和 search-manifest.ts 读取以数字加点开头的编号并升序排列。它不决定首页按日期展示的顺序。 |
sidebarSort | 文章 frontmatter,正整数 | 文章在侧边栏和搜索清单中的顺序 | 优先级高于文章文件的数字前缀;数字越小越靠前。只对文章排序,不会给整个一级分类排序。 |
date | 文章 frontmatter | 首页文章流、分类、标签、归档的时间顺序 | 主题按日期从新到旧排序。统一写 YYYY-MM-DD HH:mm:ss;不要用它控制侧边栏。 |
sticky | 文章 frontmatter | 首页和按日期排序的文章流中置顶 | 非零值会排在同组普通文章前面;多个 sticky 按数值升序,再按日期排序。它经常和 date 配合使用,但不是侧边栏排序。 |
top | 文章 frontmatter,通常写 top: 1 | 首页精选文章卡片 | 只要值为真就会进入精选候选,最终显示数量由主题 topArticle.limit 控制。它不等于 sticky,也不改变侧边栏。 |
| 目录名数字前缀 | docs/posts/ 下的子目录 | 二级目录的侧边栏/搜索清单顺序 | 例如 10.工具。一级分类的先后由两份 categoryOrder 决定,不由目录名单独决定。 |
一个常见误区是:给文章加上 date 就以为左侧栏会按日期排。实际侧边栏由 sidebar.ts 读取文件名编号或 sidebarSort;日期只用于主题的文章流和归档。反过来,只改文件名编号也不会改变首页文章顺序。
如果目录名使用了数字前缀,例如 docs/posts/技术笔记/10.工具/,目录页的 path 必须写成目录的真实名称:
path: posts/技术笔记/10.工具不要写成 posts/技术笔记/工具,除非文件系统里的目录确实叫「工具」。新手如果不想同时维护编号和真实路径,建议目录名保持无编号的稳定名称,只用文章文件名的 NN. 前缀控制文章顺序。
新增、修改、删除时应该改哪里
| 你的操作 | 必须改的位置 | 通常还要检查 |
|---|---|---|
| 在已有目录新增文章 | docs/posts/<已有目录>/NN.slug.md | 图片目录、permalink、首页/搜索/分类/标签/归档 |
| 新增二级目录 | docs/posts/<父目录>/<新子目录>/ 和 docs/catalog/<父目录>/<新子目录>.md | 目录页 path、Nav 子菜单(可选)、Features(可选) |
| 新增一级分类 | 新的 docs/posts/<分类>/、docs/catalog/<分类>.md、Nav.ts、两份 categoryOrder | Features.ts(需要首页卡片时)、第一篇文章、所有链接 |
| 修改文章标题或描述 | 文章 frontmatter 和同页 H1(标题变化时) | 首页卡片、侧边栏文字、SEO 描述、搜索 |
| 修改文章分类或标签 | 文章 frontmatter,必要时移动文件 | /categories、/tags、对应目录页、旧链接 |
| 修改文章 permalink | 文章 frontmatter、所有站内引用 | 托管平台旧地址重定向、sitemap、RSS、搜索 |
| 重命名或移动文件 | 文件路径;如果公开 URL 不变则保留原 permalink | 图片相对引用、侧边栏顺序、Git 历史 |
| 新增图片 | docs/public/images/posts/<slug>/ 和文章引用 | alt、路径大小写、构建后的图片是否存在 |
| 删除文章 | 文章文件、图片、所有手工链接 | 目录页、侧边栏、首页、分类、标签、归档、搜索、RSS、旧 URL |
| 删除一级分类 | posts 目录、catalog 目录页、Nav.ts、两份 categoryOrder、Features.ts | 所有文章 permalink、旧目录 URL、重定向 |
| 修改分类顺序 | sidebar.ts 和 search-manifest.ts 的 categoryOrder | Nav 顺序是否也要同步、重启开发服务器 |
这里的「通常还要检查」不是都要手工修改。侧边栏、目录列表、首页、分类、标签和归档大多会自动重新扫描;真正需要手工维护的是 Nav.ts、Features.ts、两份 categoryOrder 以及其他 Markdown 中手写的链接。下一篇内容、数据与页面联动关系会把自动和手动部分逐项拆开。
常见结构错误与后果
| 错误写法 | 会发生什么 | 正确做法 |
|---|---|---|
docs/posts/技术笔记/index.md | 侧边栏和搜索清单主动跳过;可能和目录路由冲突 | 改成 01.article-slug.md,目录入口放 docs/catalog/ |
文章没有 permalink | 可能按源文件路径生成路由,不能得到稳定可读的公开地址 | 在首次启动开发服务器前显式写 /posts/... |
两个页面写同一个 permalink | 构建或路由冲突,至少一个页面无法按预期访问 | 全站搜索旧值,改成唯一地址 |
写给浏览器看的地址带 .html | cleanUrls: true 下链接可能与实际路由不一致 | 站内链接统一省略 .html |
目录页 path: /posts/技术笔记 | 当前 path.join 通常仍能解析,但与团队约定的相对路径写法不一致,跨环境时不易排查 | 统一写成 path: posts/技术笔记 |
目录页 path 指向不存在的目录 | 目录页打开后看不到文章 | 先在 docs/posts/ 创建真实目录,再让 path 精确匹配 |
正文图片放 docs/posts/... | 图片不会作为公共静态资源复制到 /images/... | 放到 docs/public/images/posts/<slug>/ |
图片引用写成 /public/images/... | docs/public 已映射为根路径,多写 /public 会 404 | 写成 /images/posts/<slug>/<file>.webp |
修改了 docs/.vitepress/dist/ 里的 HTML | 下一次构建被覆盖,改动丢失 | 改对应的 docs/posts 或 docs/catalog 源文件 |
| 新增一级分类只建了文件夹 | 顶部导航、分组顺序或首页卡片不会自动出现 | 按目录页流程同步 Nav、两份 categoryOrder 和可选 Features |
| 发布后改了 permalink 却没做重定向 | 旧链接和外部收藏全部失效 | 尽量不改;必须改时更新链接并配置托管平台重定向 |
在 posts 下手改 search-manifest.json | 下次启动被 search-manifest.ts 重新生成覆盖 | 改 frontmatter 或 search-manifest.ts 源码 |
结构检查清单
新增或调整结构后,按顺序检查:
- 文章只在
docs/posts/,目录页只在docs/catalog/,没有在 posts 中使用index.md。 - 公开地址来自显式且唯一的
permalink,不是随机/pages/...。 - 文章 URL 以
/posts/开头,目录页 URL 以/catalog/开头。 - 所有站内链接都使用
/path,没有.html、反斜杠或多余空格。 - 文章的 H1 与
title一致,并且整页只有一个 H1。 - 一级目录与
categories对应;二级目录不误写成新分类。 - 正文图片在
docs/public/images/posts/<slug>/,引用以/images/...开头且每张有 alt。 - 如果写了
coverImg,它是明确存在的路径;如果故意省略,已确认首页随机封面和分享图行为符合预期。 - 侧边栏排序意图明确:文章用
NN.或sidebarSort,目录顺序有需要时使用数字前缀或categoryOrder。 - 首页时间排序使用合法
date,置顶使用sticky,精选卡片使用top,没有混用。 - 新增一级分类时,
Nav.ts、sidebar.ts和search-manifest.ts已同步,Features 按需更新。 - 没有修改
dist、cache、search-manifest.json或stats.html。 - 开发环境、生产构建、SEO 审计和预览都通过,再提交并推送。
结构正确后,下一关是理解字段会传播到哪些页面。请继续阅读内容、数据与页面联动关系。

