摆摊AI 产品实战手册

摆摊站点构建实录

这个网站本身: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.21better-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'],

这个坑值三条教训:

  1. 验证工具必须和用户处在同一视角。 curl 看到的世界和浏览器不一样, 差一个请求头,结论完全相反。
  2. 连续两次修复无效,就该停下来换取证方式,而不是提出第三个猜测。
  3. 无头浏览器(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-rewritelocation, 说明中间件的 rewrite 没有被服务器内部消化,而是被降级成了对外跳转。 先怀疑第三方库拼绝对地址的问题,换成官方的 nextUrl.clone() 模式重写—— redirect 分支好了,放行分支好了,唯独 rewrite 还是 308。 至此结论收敛:坏的不是写法,是 standalone 下"中间件 rewrite"这个机制本身 (nextUrl 的主机名和服务器自认的主机名对不上)。

修法:不修它,绕开它。i18n 前缀路由从中间件搬到 next.configredirects() + 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 搭自己的第一个产品,这份清单大概率能替你省下几天。

On this page