构建与部署
本地构建、静态导出验证,以及 Cloudflare Pages 上线步骤。
本站是独立的 Next.js + Fumadocs 应用,产物为纯静态文件,不依赖 Node 运行时服务器。
本地开发
在仓库根目录进入 site/:
cd site
pnpm install
pnpm dev开发服务器默认在 http://localhost:3000。修改 content/docs/ 下的 MDX 后会热更新。
静态构建
在仓库根目录执行(下面两条等价,任选其一):
# 方式一:先进目录再构建
cd site
pnpm build
cd .. # 回到仓库根,下一步的 serve 命令要用# 方式二:直接指定目录,不用 cd
pnpm --dir site buildnext.config.mjs 已开启 output: 'export',构建结果写入 site/out/。产物特点:
- 每个页面都有对应的
.html与同名.txt(纯文本,便于 AI agent 抓取) - 全文搜索索引在
out/api/search/,浏览器端计算,无需服务端 - 可直接丢到任意静态托管(Cloudflare Pages、Nginx、GitHub Pages 等)
本地预览导出结果(在仓库根目录执行,不要留在 site/ 里,否则会去找 site/site/out):
# 任意静态服务器即可,例如:
npx serve site/outCloudflare Pages 构建时必须设置环境变量 NODE_VERSION=22。Fumadocs 要求 Node ≥ 22,而 Pages 默认 Node 版本偏低,不设会直接构建失败。
上线到 Cloudflare Pages
目标域名:docs.163366.xyz。
在 Cloudflare Pages 中新建项目,连接 GitHub 仓库 Mutx163/mikcb。
配置构建:
| 项 | 值 |
|---|---|
| Root directory(根目录) | site |
| 构建命令 | corepack enable && pnpm install --frozen-lockfile && pnpm build |
| 输出目录 | out |
环境变量 NODE_VERSION 建议写明确版本(如 22.16.0)而不要写 22,理由见下。
为什么必须改 Root directory(2026-09-13 真实故障)
原配置是「根目录留空(= 仓库根)+ 命令 cd site && pnpm i && pnpm build」,
在 Cloudflare 上直接失败:
Executing user command: cd site && pnpm install && pnpm build
No preset version installed for command pnpm
Please install a version by running one of the following:
asdf install nodejs 22.22.0
or add one of the following versions in your config file at /opt/buildhome/.tool-versions
nodejs 14.21.3 / 16.20.2 / 18.17.1 / 20.19.0 / 22.16.0
Failed: Error while executing user command. Exited with error code: 126根因有两层,第二层才是这次真正踩到的那个:
-
根目录留空 = 整个仓库被当成根目录。 官方文档写得很直白: 「根目录不填时 Cloudflare 假定整个 git 仓库就是根目录;像 monorepo 这种 一个仓库里多个项目的情况必须指定根目录」 (Build configuration)。 而本仓库的
package.json与pnpm-lock.yaml都在site/下,根目录什么都没有。 -
Node 被解析到了预设表之外的版本。 日志里 Node 装的是 22.22.0, 但 asdf 列出的可用预设只有
… 20.19.0 / 22.16.0,22.22.0 不在其中。 CF 构建镜像的工具(含 pnpm)是按 Node 预设配套提供的,Node 落在预设之外 就一并拿不到 pnpm,于是pnpm命令根本不存在 → 退出码 126。 这就是NODE_VERSION=22(会解析成最新的 22.x)比写死22.16.0更危险的原因。
另外,pnpm 本身在镜像里是预装的:v2 镜像默认 8.7.1、v3 镜像默认 10.11.1,
覆盖变量是 PNPM_VERSION(Build image)。
但 v3 镜像的已知限制里明确写着「不支持按 pnpm-lock.yaml 探测 pnpm 版本」,
所以不要指望它自动挑对版本。
因此推荐:根目录设成 site,并用 corepack enable 让 pnpm 版本由
site/package.json 的 "packageManager": "pnpm@11.22.0" 决定。这与生成
pnpm-lock.yaml 的版本严格一致,避免用镜像默认的 10.x 去装 11.x 生成的锁文件
而触发 --frozen-lockfile 失败。
备选(必须保留「根目录 = 仓库根」时): 保持命令
cd site && pnpm install && pnpm build、输出site/out, 然后加两个环境变量:NODE_VERSION=22.16.0(落在预设表内)与PNPM_VERSION=<与 lockfile 生成版本一致的值>。
已知问题:__next.* 预取载荷 404(Cloudflare Pages 路径匹配差异)
Next.js 静态导出把 RSC 预取载荷落盘为带字面 $ 的目录嵌套形式:
site/out/docs/guide/quick-start/__next.docs/$oc$slug/__PAGE__.txt而客户端预取请求用的是点号压平形式:
GET /docs/guide/quick-start/__next.docs.$oc$slug.__PAGE__.txtsite/public/_redirects 里有两条把点号形式 200 重写到嵌套形式的规则。
⚠️ 已复测:这两条规则是空转的,修不了这个 404(2026-09-13,新构建上线后实测)。
请求 结果 /docs/guide/quick-start/__next.docs.txt200 ✅ /docs/guide/quick-start/index.txt200 ✅ …/__next.docs/$oc$slug/__PAGE__.txt(字面$)404 ❌ …/__next.docs/%24oc%24slug/__PAGE__.txt(百分号编码)404 ❌ …/__next.docs.$oc$slug.__PAGE__.txt(点号压平)404 ❌ /__next.!KGhvbWUp/__PAGE__.txt404 ❌ 关键事实:那个
$oc$slug/__PAGE__.txt文件确实在构建产物里(site/out/…下能直接找到),但 Cloudflare 不服务$oc$slug/这条路径。所以规则把一个 404 重写成另一个 404,不可能生效。原注释里「嵌套形式 → 200」很可能是本地python3 -m http.server --directory site/out测出来的。本地文件服务器不介意$,Cloudflare 介意。影响面很小:软导航正常(走
index.txt/__next.*.txt,都是 200),只是控制台 多一条预取 404、预取退化为按需拉取。要真正修掉只有两条路(都未做):a) 用 Pages Function +
ASSETS绑定自己做 点号形式→$路径的映射(不再指望静态服务器命中);b) 向 Next / Cloudflare 反馈「$目录名在 Pages 静态资源上 404」。别再往_redirects里加规则, 重写目标本身不可达。
部署完成后,在 Pages 项目的自定义域中添加 docs.163366.xyz,按提示完成 DNS。
之后主分支上对 site/ 的推送会自动触发重新构建与发布。
上线检查清单(换域名 / 长期间未部署后逐条核对)
2026-09 换域名(mutx.ccwu.cc → 163366.xyz)时踩过的坑,逐条对应一个真实故障:
| 检查项 | 怎么查 | 出现过的问题 |
|---|---|---|
| 文档站是否真的重新构建过 | curl -s https://docs.163366.xyz/ | grep canonical | 源码已加 canonical/sitemap/llms.txt,但 Pages 仍是旧构建,全部 404 |
| 文档站 sitemap 可达 | curl -sI https://docs.163366.xyz/sitemap.xml | 构建过期时为 404,24 篇文档页对搜索引擎零入口 |
| 旧域名 301 是否一步到 https | curl -sI https://mutx.ccwu.cc/ | grep -i location | 曾一度跳到 http://163366.xyz/(明文中间跳),应为 https:// 直达 |
www 是否解析 | getent hosts www.163366.xyz | 无记录(NXDOMAIN)。若 GSC 属性建在 www 上,站点地图永远读不到 |
| 站点地图能否被读 | curl -s https://163366.xyz/sitemap.xml | python3 -c 'import xml.dom.minidom,sys;xml.dom.minidom.parseString(sys.stdin.read());print("ok")' | 文件本身一直合法,问题在 GSC 侧属性状态 |
换域名后必做:GSC 里对应重新验证属性(旧验证随域名失效)、重新提交站点地图、 核对属性是「网域」还是「网址前缀」且与 301 目标一致。
维护本站时的已知坑
改这个站之前建议先读一遍,都是实际踩过的:
-
fumadocs-mdx15 生成的.source/没有index.ts
从@/.source/server导入,不要写@/.source。 -
MDX 里的裸
<会被当成 JSX 标签
例如版本区间>=3.12.2 <4.0.0,<4.0.0要用反引号包住,或改写为<。 -
Step/Steps不在默认 MDX 组件里
已在根目录mdx-components.tsx手动注册;新增同类组件时同样要挂进去。 -
pnpm 11 默认不跑依赖的构建脚本
esbuild等需要构建产物的包,已在pnpm-workspace.yaml的allowBuilds中放行。新增需要 postinstall 的依赖时同步更新该文件。 -
.source/、.next/、out/不要提交
已由site/.gitignore排除;out/由 Cloudflare 在构建机上生成。 -
app/favicon.ico会覆盖public/favicon.ico
Next 把app/favicon.ico当作文件约定(等价于一条/favicon.ico路由), 路由优先于public/静态文件。2026-09-12 曾在public/下新增一份favicon.ico,它从未被服务过(out/favicon.ico始终等于app/favicon.ico)。 要换图标请改app/favicon.ico;public/下只放不被 app 约定接管的资源 (favicon-48.png这类带尺寸的 PNG 不受影响)。