Cloudflare Pages + Next.js 踩坑日记
Monorepo 里一批 Next.js 应用部署到 Cloudflare Pages 时遇到的问题记录:next-on-pages 与 Vercel Build Adapter、动态路由 Edge 报错、25 MiB 单文件限制、turbo 缓存与 Node 版本。持续更新。
一个 monorepo 里有十来个 Next.js 应用,大部分部署在 Cloudflare Pages 上,构建走 @cloudflare/next-on-pages。这里按时间记录部署过程中踩到的坑,后面遇到新问题继续往下追加。
| # | 问题 | 日期 | 结论 |
|---|---|---|---|
| 1 | next-on-pages 突然构建失败 | 2026-09-23 | 构建命令加 NEXT_ENABLE_ADAPTER=0 |
| 2 | 动态路由报 not configured to run with the Edge Runtime | 2026-10-21 | 纯浏览器应用用 output: 'export' |
| 3 | wasm 超过 Pages 25 MiB 单文件限制 | 2026-10-21 | 固定 @huggingface/transformers@4.2.0 |
| 4 | turbo 缓存不含 dist 与 Node 版本告警 | 2026-10-21 | outputs 加 dist/**,固定完整 Node 版本 |
坑 1:next-on-pages 突然构建失败
一个部署在 Cloudflare Pages 上的 Next.js 16 站点,上周还能正常构建,这周代码和 lockfile 都没怎么动,构建却直接失败。问题不在项目本身,而在 @cloudflare/next-on-pages 内部调用的 Vercel CLI:它不锁版本,而新版 Vercel CLI 对 Next.js ≥ 16.2 默认改走 Next.js Build Adapter,产物结构变了,已归档的 next-on-pages 认不出来。
构建命令前加上 NEXT_ENABLE_ADAPTER=0,关闭 Vercel CLI 的 adapter 流程:
NEXT_ENABLE_ADAPTER=0 npx --legacy-peer-deps @cloudflare/next-on-pages@1项目背景
- Next.js 16.3.5,App Router,页面基本都是静态或 SSG
- 唯一的动态路由是
app/og/route.tsx,声明了export const runtime = 'edge',用ImageResponse生成 OG 图 - Cloudflare Pages 配置:
- 构建命令:
npx --legacy-peer-deps @cloudflare/next-on-pages@1 - 构建输出:
.vercel/output/static
- 构建命令:
站点必须留在 Pages:迁到 Workers 后,自动分配的域名会从 *.pages.dev 变成 *.workers.dev,而且手上还有很多项目处境相同。
现象
构建日志最后报错:
⚡️ ERROR: Failed to produce a Cloudflare Pages build from the project.
⚡️
⚡️ The following routes were not configured to run with the Edge Runtime:
⚡️ - /_global-error
⚡️ - /_not-found
⚡️ - /blocks/[category]
⚡️ - /docs/[id]
⚡️ - /docs/introduction.segments/_full.segment
⚡️ ...
⚡️ Please make sure that all your non-static routes export the following edge runtime route segment config:
⚡️ export const runtime = 'edge';奇怪的是,列表里的路由在 next build 的输出中全是 ○(静态)或 ●(SSG),只有 /og 是 ƒ(动态)。为什么 next-on-pages 会把它们当成没声明 Edge 的动态路由?
排查:对比两次构建日志
| 成功(09-16) | 失败(09-23) | |
|---|---|---|
| 项目依赖 | Next 16.3.5,lockfile 一致 | 相同 |
| next-on-pages | 1.13.16 | 1.13.16 |
| Vercel CLI | 59.18.0(命中缓存) | 59.25.4(npx 重新下载最新版) |
CLI 内置的 @vercel/next | 12.0.2 | 12.0.5 |
| 日志特征 | Traced Next.js server files、Created all serverless functions | Applying modifyConfig from Vercel、Running onBuildComplete from Vercel |
唯一的变化是 Vercel CLI。而失败日志里新出现的 modifyConfig、onBuildComplete,正是 Next.js Build Adapter API 的两个钩子。
根因
next-on-pages 自己并不构建 Next.js,它做两件事:
- 执行
npx vercel build,生成.vercel/output(Vercel Build Output API); - 把这份产物转换成 Pages 的
_worker.js和静态资源。
第 1 步没有锁版本,每次构建用的都是最新的 Vercel CLI。
@vercel/next 里决定是否走 adapter 的逻辑如下:
const isAdapterEnabled =
// integration tests expect outputs object
!process.env.NEXT_BUILDER_INTEGRATION &&
process.env.NEXT_ENABLE_ADAPTER === '1' &&
semver.gte(nextVersion, MINIMUM_NEXT_ADAPTER_VERSION) // 'v16.2.0-canary.28'对 Next.js ≥ 16.2.0-canary.28,NEXT_ENABLE_ADAPTER=1 已经是平台默认值。从失败日志看,新版 CLI 在本地执行 vercel build 时也启用了它。走 adapter 之后:
- SSG 页面不再输出为带 fallback HTML 的 prerender 配置(next-on-pages 能识别成静态资源),而是变成函数加新格式的 prerender 元数据(
prerenderClassification被换成了initialMetadata); experimental.collapseAdapterRoutes默认开启,多个动态路由可能共用一个路由表条目;- Next.js 16 的 segment 预取文件(
*.segments/*.segment.rsc)作为独立产物出现。
next-on-pages 已于 2025-09-29 归档,没有跟进这种格式。凡是它分辨不出是静态还是 Edge 的产物,都会被列进 “not configured to run with the Edge Runtime”。
修复
方式一:关闭 adapter(推荐)
直接写进构建命令,方便在多个项目间统一复制:
NEXT_ENABLE_ADAPTER=0 npx --legacy-peer-deps @cloudflare/next-on-pages@1也可以在 Pages 控制台配置:Workers & Pages → 项目 → Settings → Variables and Secrets,添加 Text 类型变量 NEXT_ENABLE_ADAPTER = 0,Production 和 Preview 都要加,然后在 Deployments 里 Retry deployment。
看构建日志:
- 不再出现
Applying modifyConfig from Vercel、Running onBuildComplete from Vercel - 重新出现
Created all serverless functions和 next-on-pages 的Build Summary
这个关闭方式来自 vercel/vercel#17640。
方式二:锁定 Vercel CLI 版本
pnpm add -D vercel@59.18.0next-on-pages 执行的是 npx vercel build,npx 会优先使用项目里 node_modules/.bin/vercel,不再拉最新版。和方式一一起用,可以同时防住 adapter 默认开启和其他 CLI 变动。
为什么不直接适配新格式
项目多的时候,fork 一份 next-on-pages 自己维护听起来很合理。但要支持 adapter 的产物,需要:
- 重写静态和动态的判定逻辑,识别新的 prerender 元数据;
- 拆开
collapseAdapterRoutes合并后的路由; - 把
.segments/*.segment.rsc映射成静态资源和对应的路由规则; - 追一个还在 beta 的格式:
@next-community/adapter-vercel仍是0.0.1-beta.x,在 Vercel 自己的平台上也还有 bug。
代码改动量不算大,真正的成本在于持续跟进变化。现阶段 fork 只需要做两件事:锁定 Vercel CLI 版本,默认设置 NEXT_ENABLE_ADAPTER=0。
等 Vercel 移除旧的构建流程,或者锁定的旧 CLI 不再支持新版 Next.js 时,这个开关就会失效。
留在 Pages 的长期方案
- 静态导出:站点除个别路由外都是 SSG 的话,用
output: 'export',OG 图改用opengraph-image.tsx+generateStaticParams在构建时生成,部署out/目录。不依赖适配器,也没有 Worker,域名不变。 - 静态导出 + Pages Functions:
/og?title=...这类必须在运行时处理的接口放到functions/目录,比如用workers-og或@cloudflare/pages-plugin-vercel-og。 - 面向 Pages 的 Next.js adapter:旧流程被移除后,与其适配 Vercel adapter 的产物,不如通过 Next.js 的
adapterPath写一个直接面向 Pages 的 adapter,绕过 Vercel CLI。这也是 OpenNext 正在走的方向。 - OpenNext 放进 Pages(未验证):
@opennextjs/cloudflare官方只支持 Workers。理论上可以借助 Pages 的 advanced mode(_worker.js+env.ASSETS,开启nodejs_compat)承载它的产物,但官方不支持,也没有实测过。
参考
- cloudflare/next-on-pages(已归档,官方推荐迁移到 OpenNext)
- vercel/vercel#17640:
NEXT_ENABLE_ADAPTER=0关闭方式 - vercel/next.js#98218:
collapseAdapterRoutes默认开启 - RFC: Deployment Adapters API
- nextjs/adapter-vercel
@vercel/next源码- OpenNext for Cloudflare
坑 2:动态路由报 Edge Runtime 错误
背景:把 9 个 Pages 应用统一成同一种构建方式,去掉 @cloudflare/next-on-pages 依赖和 output: 'export',构建命令统一为 NEXT_ENABLE_ADAPTER=0 npx --legacy-peer-deps @cloudflare/next-on-pages@1,输出 .vercel/output/static。其中 8 个构建成功,只有 bycut 失败:
⚡️ ERROR: Failed to produce a Cloudflare Pages build from the project.
⚡️ The following routes were not configured to run with the Edge Runtime:
⚡️ - /[locale]/editor
⚡️ - /[locale]/projects排查:
- 本地去掉
output: 'export'后next build完全正常,所有路由都是●(SSG,/en/editor、/zh/editor等都已预渲染),说明问题不在 Next.js,而在 next-on-pages 这一步。 - next-on-pages 先跑
vercel build。查看.vercel/output/functions发现,[locale]/editor.func的.vc-config.json里operationType是ISR、运行时是nodejs24.x。也就是说 Vercel 给每个带动态段的路由额外生成了一个 Node 兜底函数,处理没预渲染过的 locale。 - next-on-pages 不接受 Node 函数,于是报“没有配置 Edge Runtime”。
- 为什么另外几个应用没事:它们(byplay、vidl、dropply-web)只有一层
[locale]/page.tsx,没有[locale]/editor这样的二级动态路由。bycut 是唯一有二级路由的。这一点是对照目录结构得出的推断,没有拿其他应用实际对比过日志。
尝试过但无效的:在 [locale]/layout.tsx 加 export const dynamicParams = false,想去掉兜底函数,报错不变。
没采用的方案:直接在两个页面加 export const runtime = 'edge'。这两个页面是 'use client',路由段配置不能写在客户端组件里,要加在上层 Server Component 的 layout 上;而且声明 Edge 后 next-intl 的 middleware.ts 会真正跑在 Edge 上,语言重定向行为会变。对一个纯浏览器应用来说得不偿失。
结论:bycut 的页面全部在构建期预渲染,没有任何服务端逻辑,保持 output: 'export',构建命令 pnpm --filter bycut run build,输出 apps/bycut/out。是否需要 next-on-pages,看应用有没有真正的服务端路由。
坑 3:wasm 超过 25 MiB 单文件限制
现象:依赖整体升级到最新后,bycut 构建失败:
✘ [ERROR] Error: Pages only supports files up to 25 MiB in size
_next/static/media/ort-wasm-simd-threaded.asyncify.0phdreyubz4lg.wasm is 25.6 MiB in size原因:@huggingface/transformers 从 4.2.0 升到 4.3.1,间接依赖的 onnxruntime-web 从 1.26 升到 1.31,其中 ort-wasm-simd-threaded.asyncify.wasm 从 23.5 MB(约 22.5 MiB)涨到 26.9 MB(25.6 MiB),刚好超过 Pages 单文件 25 MiB 的上限。
范围:不只 bycut(Whisper 转录),clearify(背景移除)也用 transformers,构建产物里同样有这个文件,也会失败。
4.3.x 更新了什么:4.3.0 有结构化输出、三个新模型、Safari 26 的 WebGPU、Whisper 进度回调修复;4.3.1 只加了 EmbeddingGemma 2。对这两个应用没有非升不可的理由。发布说明里没有提 wasm 体积变化,是对比包内文件得出的。
处理:catalog 里固定 '@huggingface/transformers': 4.2.0,不带 ^,避免被自动升级。解除条件:新版本的 wasm 低于 25 MiB,或者改成从 CDN 加载 wasm。
依赖里带大体积 wasm 或二进制资源时,升级后看一眼构建产物里单个文件是否超过 25 MiB,Pages 的限制是硬性的。
坑 4:turbo 缓存不含 dist 与 Node 版本告警
现象一:构建日志里 prepare 阶段出现:
WARNING no output files found for task @cdlab/cipher#build. Please check your `outputs` key in `turbo.json`cipher、db、uncrypto、utils 都有。
原因:这几个包用 tsdown 构建,产物在 dist/,而 turbo.json 里 build 任务的 outputs 只写了 .next/**。turbo 不缓存 dist/,命中缓存(FULL TURBO)时只重放日志,不恢复 dist/,下游应用就可能找不到这些包的产物。
处理:outputs 加上 dist/**。本地删掉 packages/utils/dist 重跑 turbo build,命中缓存后 dist/ 被恢复,告警消失。
现象二:
npm warn EBADENGINE Unsupported engine {
package: 'corepack@0.36.0',
required: { node: '^22.22.2 || ^24.15.0 || >=26.0.0' },
current: { node: 'v24.13.1' }
}原因:.node-version 写的是 24,Cloudflare 解析成它内置的 24.13.1,低于 corepack 要求的 ^24.15.0。
处理:.node-version 改成完整版本 24.19.0。Cloudflare 是否能装指定的小版本,要看首次构建日志确认。
小结
- 凡是依赖
npx拉最新版的构建步骤,都要想到它可能在你不动代码的情况下变化(坑 1)。 - 部署目标的限制要提前对照:Pages 不接受 Node 函数(坑 2),单文件不能超过 25 MiB(坑 3)。
- monorepo 里被其他包依赖的构建产物,要写进 turbo 的
outputs(坑 4)。