摆摊站点构建实录
这个网站本身:Next.js 16 + Fumadocs 从零搭到能用,9 个真实踩过的坑。
这个站是用 AI 结对搭出来的(Claude Code),从空目录到手册、搜索、登录、主题全部就位。 过程不顺利——9 个坑,每一个都真实消耗了排查时间。原样记录,包括排查思路和修法。
技术栈:Next.js 16(App Router + Turbopack)、Fumadocs 16、Tailwind 4、 Better Auth、Drizzle + PostgreSQL、Orama 搜索。
坑 1:better-auth 的 CLI 拖垮整个构建
现象:构建报错 Export kAPIErrorHeaderSymbol doesn't exist in target module,
指向 better-call 包。
排查:@better-auth/cli(用来生成数据库 schema 的官方工具)稳定版停在 1.4.21,
它依赖旧的 better-auth@1.4.21 → better-call@1.1.8。而主依赖 better-auth@1.6.26
需要 better-call@1.3.7。pnpm 的 peer 解析把新核心链到了旧包上。
修法:移除 CLI,schema 手写。Better Auth 只有四张表,
对照 @better-auth/core/dist/db/get-tables.mjs 里的字段定义逐个核对即可。
工具版本落后于主库时,果断放弃工具。
坑 2:fumadocs-mdx 生成的文件名变了
现象:Module not found: Can't resolve '@/.source'。
根因:网上教程(和 AI 的训练数据)都写 import { docs } from '@/.source',
但 fumadocs-mdx 15.2 生成的是 .source/server.ts,没有 index.ts。
修法:import { docs } from '@/.source/server'。
教训:装完包先看它实际生成/导出了什么,别信教程里的路径。
一句 ls .source/ 能省二十分钟。
坑 3:i18n 配置对象过不了 RSC 边界
现象:预渲染报错 Functions cannot be passed directly to Client Components。
根因:defineI18n() 返回的对象带一个 translations 函数。
把它整个传给 <DocsLayout i18n={...}>(客户端组件),函数无法序列化。
修法:查类型定义发现 i18n?: boolean | I18nConfig,传 true 即可,
布局会从 context 里自己拿配置。RSC 时代的新习惯:
传给客户端组件的 props,先问一遍"这东西能 JSON 化吗"。
坑 4:全站按钮看起来都不可点
现象:所有按钮悬停都是箭头光标,用户以为控件坏了。
根因:Tailwind v4 的 Preflight 移除了 button 的默认 cursor: pointer
(v3 还有)。整个编译产物里 cursor:pointer 出现 0 次。
修法:在 @layer base 补一条:
@layer base {
button:not(:disabled),
[role='button']:not(:disabled) {
cursor: pointer;
}
}升级大版本时,被移除的默认值比新增的 API 更危险——没有报错,只有变糟的体验。
坑 5:中文搜索默默失效
现象:搜索功能"正常",但中文查询几乎搜不到东西。
根因:Orama 默认分词器按空格切词,中文整段变成一个 token。 没有报错,没有警告,只是结果永远为空——这种静默失效比崩溃难发现得多。
修法:给搜索接口挂中文分词器:
import { createTokenizer } from '@orama/tokenizers/mandarin';
export const { GET } = createFromSource(source, {
localeMap: {
zh: { components: { tokenizer: createTokenizer() } },
},
});坑 6:Google Fonts 在国内是构建期炸弹
现象:(预防性规避,没让它发生)next/font/google 构建期要连 Google 拉字体,
国内 CI 或服务器上会超时甚至构建失败。
修法:字体文件下载进仓库自托管(latin 子集一共 116KB),@font-face 引用。
另注意:西文装饰字体都是纯拉丁字库,字体栈必须带完整 CJK 回退,
否则中文会掉到浏览器默认宋体。
坑 7:最大的坑——页面完好,但整站没有一个按钮是活的
现象:页面渲染正常、样式正常、光标正常,但点任何东西都没反应。 先后修了光标、选中态、重写了组件——都没用,因为都没打中。
转折点:不再猜,装 Playwright 无头浏览器真实复现。第一次跑就拿到证据:
HYDRATED: false ← 页面从未完成水合
5 个 JS chunk → 403 Forbidden根因:Next 16 的 dev 服务器对 /_next/* 有跨源保护,默认只放行 localhost。
用 127.0.0.1 打开时,水合阶段动态 import() 的 chunk 带 Origin 头,被 403;
<script> 标签的静态 chunk 不带 Origin 头,正常加载。
于是 SSR、CSS、部分 JS 都好好的——页面看起来完好,就是永远不水合。
更阴险的是:用 curl 检查同一个 URL 返回 200(curl 不带 Origin 头), 所以命令行验证全部失真。
修法:
// next.config.mjs(仅影响 dev)
allowedDevOrigins: ['127.0.0.1', '192.168.1.x'],这个坑值三条教训:
- 验证工具必须和用户处在同一视角。 curl 看到的世界和浏览器不一样, 差一个请求头,结论完全相反。
- 连续两次修复无效,就该停下来换取证方式,而不是提出第三个猜测。
- 无头浏览器(Playwright)是排查"看起来正常但不工作"的终极手段, 装一个只要几分钟,比猜三轮便宜得多。
坑 8:pnpm 严格模式下的传递依赖
现象:自己写的组件 import { useTheme } from 'next-themes' 构建失败。
根因:next-themes 是 fumadocs-ui 的依赖,不是项目的直接依赖,
pnpm 默认不做幽灵依赖提升。
修法:不要顺手 pnpm add next-themes——那可能装出第二个实例,
React context 对不上,setTheme 变成静默空操作,比报错难查十倍。
正确做法是用上游的再导出:import { useTheme } from 'fumadocs-ui/provider/base',
保证和框架用的是同一个实例。
坑 9:dev 一切正常,standalone 部署后 308 死循环
现象:next build 的 standalone 产物跑起来后,所有页面返回 308,
curl -L 直接因为"跳转次数过多"退出——跳转目标是页面自己。
同一份代码在 dev 下完全正常。
排查:响应头里同时出现 x-middleware-rewrite 和 location,
说明中间件的 rewrite 没有被服务器内部消化,而是被降级成了对外跳转。
先怀疑第三方库拼绝对地址的问题,换成官方的 nextUrl.clone() 模式重写——
redirect 分支好了,放行分支好了,唯独 rewrite 还是 308。
至此结论收敛:坏的不是写法,是 standalone 下"中间件 rewrite"这个机制本身
(nextUrl 的主机名和服务器自认的主机名对不上)。
修法:不修它,绕开它。i18n 前缀路由从中间件搬到 next.config 的
redirects() + rewrites()——配置级路由由路由核心处理,
不经过中间件,dev / standalone / 任何部署模式行为一致:
async redirects() {
return [{ source: '/zh/:path*', destination: '/:path*', permanent: true }];
},
async rewrites() {
return {
beforeFiles: [
{ source: '/', destination: '/zh' },
{ source: '/:path((?!en(?:/|$)|zh(?:/|$)|api(?:/|$)|_next|.*\\..*).*)',
destination: '/zh/:path' },
],
};
}副产品:中间件整个删掉了,每个请求少一次函数调用。
教训:dev 通过不等于部署形态通过。 上线前必须用真实的生产产物
(node .next/standalone/server.js)完整跑一遍冒烟——
这次它抓住了一个会让上线当天全站打不开的问题。
复盘
回头看,9 个坑里只有 1 个(中文分词)和"做什么产品"有关, 其余 8 个全是工程环境的时间税:版本错配、默认值变更、运行时行为差异。
这正是用 AI 写代码的真实体感:主功能半天就通了, 剩下的时间都花在这类"书上不写"的地方。区别在于, 带着方法排查(复现 → 取证 → 定位 → 验证)每个坑是小时级, 靠猜是天级。
如果你也在用 AI 搭自己的第一个产品,这份清单大概率能替你省下几天。