一个 Turborepo 里同时放营销站、文档站、Web 应用,2026 年是常见做法。Vercel 能把它干净地部上去,前提是每个 project 三个设置都对:Root Directory、build 命令、Ignored Build Step。配错了要么每次 push 三个 app 全部重建(慢,而且在 Pro 套餐上烧的是付费 build 分钟),要么直接报 Module not found: @workspace/ui。
本文给出截至 2026 年 6 月、Vercel 官方文档推荐的精确配置,外加两个最坑人的 bug:新分支全量重建,以及缓存命中后的”产物找不到”。
太长不看
- 一个 app 对应一个 Vercel project。一个 project 只部署一种框架的一次构建。
- app 放在
apps/下,每个 project 的 Root Directory 设成apps/<name>。 - Build 命令就写
turbo build——Turbo 1.8+ 会从 Root Directory 自动推断 filter。不用把turbo加成依赖,Vercel 上全局就有。 - Ignored Build Step:
npx turbo-ignore --fallback=HEAD^1。--fallback这个参数才是阻止每个新分支全量重建的关键。 - 共享包用
transpilePackages(Next.js)或dist/构建步骤接进来——不用发到 npm。
monorepo 怎么映射到 Vercel
一个 Vercel project 用一种框架预设部署一个 app。所以 N 个 app 的 monorepo 就是 N 个 Vercel project,每个指向同一个 Git 仓库里的不同子目录。每个 project 都监听整个仓库的 push;具体哪个 project 真去构建,由各自的 Root Directory 和 Ignored Build Step 决定。
Turborepo 负责构建图:它知道 apps/www 依赖 packages/ui,按内容 hash 缓存任务产物,并提供 turbo-ignore,让只改文档的提交不会去重建营销站。
先说一个对内容站特别要命的商用限制:Vercel 的 Hobby 套餐仅限非商业用途(Vercel 的 Fair Use Guidelines 在 2026 年 2 月再次确认)。任何挂广告、放联盟链接、或与盈利相关的站点,都必须上 Pro(每席位 20 美元/月)。一堆盈利内容站组成的 monorepo,毫无疑问属于商业用途。
配错了的征兆
- 给
apps/docs改一行,三个 project 一起重建。 - build log 报
Module not found: Can't resolve '@workspace/ui'。 - Vercel 从仓库根目录跑了 build 命令,结果部了错的 app。
turbo build成功了,但缓存命中时 Vercel 说输出目录是空的。- 每个新 PR 分支都全量重建,哪怕这个 app 啥都没改。
能跑的项目结构
my-monorepo/
├── apps/
│ ├── www/ → 营销站(Astro)
│ ├── docs/ → 文档站(Next.js)
│ └── app/ → Web 应用(Next.js)
├── packages/
│ ├── ui/ → 共享 React 组件
│ ├── config-tsconfig/ → 共享 tsconfig
│ └── eslint-config/ → 共享 lint
├── turbo.json
├── package.json → workspaces: ["apps/*", "packages/*"]
└── pnpm-workspace.yaml → 用 pnpm 时
每个 apps/<name>/package.json 把共享包当普通依赖声明:pnpm 写 "@workspace/ui": "workspace:*",npm/yarn workspaces 写 "@workspace/ui": "*"。
每个 app 的 Vercel 设置(2026 年 6 月)
下面这些就是你导入 Turborepo 时 Vercel 自动识别的值,取自官方部署文档。在每个 project 的 Settings → General 里设:
| 设置项 | 值 | 说明 |
|---|---|---|
| Root Directory | apps/www | 最常被跳过的一步;多数”module not found”都因它而起。 |
| Framework Preset | Astro / Next.js | 35+ 预设之一,自动识别。 |
| Build Command | turbo build | Turbo 1.8+ 会从 Root Directory 推断 --filter。 |
| Install Command | 用默认 | Vercel 自动识别 pnpm/npm/yarn,在仓库根目录装整个 workspace。 |
| Output Directory | 框架默认 | Astro 是 dist,Next.js 是 .next。 |
| Ignored Build Step | npx turbo-ignore --fallback=HEAD^1 | 这个 app 的依赖图没变化时跳过构建。 |
这里有两点常被搞错:
- Build 命令。 不用把
turbo装成依赖——每次 Vercel 构建时它都全局可用,而且从 Turbo 1.8 起 filter 是从 Root Directory 推断的,所以一句turbo build就够。老式长写法cd ../.. && turbo run build --filter=www也仍然能用,想显式声明就用它。 - Node 版本。 在仓库根
package.json的engines.node里锁一次,让每个 project 都用同一个大版本;否则不同 project 可能落到不同 Node 版本上。
为什么 Ignored Build Step 里要加 --fallback
光写 npx turbo-ignore 是跟上一次成功部署做对比。在一个全新分支上,这个分支还没有过任何部署,于是它保守起见就去构建——结果每个新 PR 都把所有 app 全量重建,意义全失。加上 --fallback=HEAD^1 是告诉它:没有历史部署时就跟上一个 commit 做 diff,于是只有真正改动过的 app 才构建。这正是 Vercel 导入时自己设的值,也是那个有名的”turbo-ignore 在新分支上总是构建所有 project”问题的解法。
不用 Turbo 的 workspace,手动等价写法(在每个 app 的 Root Directory 里跑):
git diff HEAD^ HEAD --quiet . ../../packages/ui ../../packages/config-tsconfig
这条命令退出 0(app 及其共享依赖都没变化)时 Vercel 跳过构建。
共享包不发布也能用
两种模式,选一种:
- 构建时转译(Next.js)。 让使用方 app 在构建时编译共享的 TypeScript:
/** @type {import('next').NextConfig} */
module.exports = {
transpilePackages: ['@workspace/ui', '@workspace/config-tsconfig'],
};
- 预构建包。 每个包有
build脚本输出到dist/,它的package.json把main/types指到构建产物。因为 build 任务在turbo.json里声明了"dependsOn": ["^build"],Turbo 会先构建依赖再构建依赖方。
内容站如果只是引一个共享组件库,模式 1 更简单。包还要发到 npm 时,模式 2 扩展性更好。
缓存命中后”产物找不到”的坑
一种常见故障:turbo build 命中 Turborepo 缓存,却没往预期的输出目录里恢复任何东西,于是 Vercel 报”构建没有产生输出”。原因几乎都是框架真实输出和 turbo.json 里 outputs 键对不上。把它们对齐:
{
"$schema": "https://turborepo.com/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**", ".vercel/output/**"]
}
}
}
本地跑一遍 turbo build,确认它产出的目录和 Vercel 对你这个框架预设的预期一致。还有两个相关注意点(截至 2026 年 6 月):Next.js 的 Skew Protection 一定会造成 Turborepo 缓存未命中(它会往 hash 里注入一个每次部署都变的环境变量);而 2.4.1 以下的 Turborepo 在 Skew Protection 下可能漏发资源,所以升级到 2.4.1 或更高。
成本:monorepo 实际用掉多少
build 分钟是大家最容易忘记估的那一项。截至 2026 年 6 月:
| 项目 | Hobby | Pro |
|---|---|---|
| 价格 | 免费(仅限非商业) | 每席位 20 美元/月,含 20 美元额度 |
| 并发构建 | 1 | 12 |
| Project 数 | 200 | 更高 |
| 每日部署 | 100 | 更高 |
| 单次构建时长上限 | 45 分钟/次 | 45 分钟/次 |
| build 分钟 | 不含 | 标准机约 0.014 美元/分钟 |
实操结论:三个 app 的盈利内容站 monorepo 应该上 Pro,而 turbo-ignore 是你压低付费 build 分钟的主要杠杆。不接它,一个 app 的 README 里改个错字,就可能触发三次全量构建。
容易踩的坑
- Root Directory 留在
/,每个 project 都构建同一个(错的)app。 - build 命令里在仓库根目录写死
npm install,没让 Vercel 识别 workspace——pnpm/yarn workspaces 会坏。 npx turbo-ignore不带--fallback——每个新分支都全量重建。turbo.json里outputs和框架真实输出目录对不上——缓存命中时”产物找不到”。- 没在根
package.json锁 Node——各 project 漂到不同 Node 大版本上。 - 两个 app 都要的环境变量只配在一个 project 里——env 在 Vercel 上是按 project 单独作用域的。
- 把商业内容站跑在 Hobby 上——违反 Fair Use Guidelines,可能被封号。
FAQ
- Vercel 部署 monorepo 一定要用 Turbo 吗?: 不用。Vercel 直接支持 npm/yarn/pnpm workspaces。Turbo 加的是缓存和
turbo-ignore。两三个 app 通常 workspaces 就够,但你会失去选择性构建。 - 一个 Vercel project 能部多个框架吗?: 不能。一个 project = 一种框架的一次构建。一个 app 开一个 project。
- 现在 Vercel 推荐的 build 命令到底是什么?:
turbo build(Turbo 1.8+,filter 从 Root Directory 推断)。显式的cd ../.. && turbo run build --filter=<app>也行。 - 为什么我的新分支总是全量重建?: 你的
turbo-ignore少了--fallback=HEAD^1。全新分支上没有历史部署,光turbo-ignore会保守地去构建。 - 用 Nx 行不行?: 形态一样——每个 app 设 Root Directory,build 命令写
nx build <app>,Ignored Build Step 里用 Nx 的 affected 命令。 - 内容站能跑在免费 Hobby 上吗?: 只有真正非商业时才行。挂广告或联盟收入就算商业用途,按 Vercel 的 Fair Use Guidelines 必须上 Pro。
相关阅读
外部参考:Vercel: Deploying Turborepo 与 Vercel Fair Use Guidelines。