Vue

Nuxt 3 预渲染与环境变量问题总结

问题描述

在首页 (/) 使用预渲染 (prerender: true) 时,页面需要调用 API 接口,而 API 的 baseURL 是通过 runtimeConfig 从环境变量中获取的。打包 Docker 镜像后运行,报错找不到 output 目录下的 index.mjs 文件。

根本原因

预渲染(Prerender)发生在构建阶段,此时:

  1. 环境变量尚未注入(Docker 运行时才会注入)
  2. runtimeConfig.public.API_BASE_URL 的值为空字符串 ''
  3. API 调用失败,导致预渲染过程中断
  4. 最终 index.mjs 文件未能正确生成
// nuxt.config.ts 原配置
runtimeConfig: {
  public: {
    API_BASE_URL: ''  // 构建时为空,运行时才从 env 注入
  }
},

routeRules: {
  '/': { prerender: true }  // ❌ 预渲染时拿不到环境变量
}

预渲染 vs 运行时渲染

特性预渲染 (Prerender)ISR / SSR
执行时机构建阶段运行阶段
环境变量可用性❌ 不可用✅ 可用
适用场景纯静态页面动态数据页面

解决方案

将预渲染改为 ISR(增量静态再生) 或 SSR:

// nuxt.config.ts 修改后
routeRules: {
  // '/': { prerender: true },  // ❌ 注释掉
  '/': { isr: 60 * 60 * 1 }     // ✅ 使用 ISR,缓存 1 小时
}

ISR 优势

  • 运行时渲染:首次请求时在服务端渲染,可以获取环境变量
  • 缓存机制:渲染结果缓存指定时间(如 1 小时),减少服务器压力
  • 自动更新:缓存过期后自动重新渲染,保证数据新鲜度

最佳实践建议

  1. 需要调用 API 的页面:使用 ISR 或 SSR,避免使用预渲染
  2. 纯静态页面:可以使用预渲染,如关于页面、隐私政策等
  3. 环境变量敏感操作:确保在运行时执行,而非构建时

相关配置参考

routeRules: {
  // 预渲染 - 仅适用于不依赖环境变量的静态页面
  '/about': { prerender: true },
  
  // ISR - 适用于需要环境变量且数据更新不频繁的页面
  '/': { isr: 3600 },           // 缓存 1 小时
  '/products': { isr: 1800 },   // 缓存 30 分钟
  
  // SSR - 适用于数据实时性要求高的页面
  '/dashboard': { ssr: true }
}

Nuxt Content + SQLite 在只读容器中的问题

问题现象

文档站在本地 build 后预览正常,但是部署到测试环境后,直接访问深层文档链接失败。

示例:

https://developer.dev.tripguru.ai/docs/mcp-agents/setup

表现为:

  1. 从首页一步一步点击进入文档目录正常。
  2. 直接通过链接访问 /docs/mcp-agents/setup 不正常。
  3. 测试环境运维配置了只读文件系统:除了代码目录外,其他目录不能写入。

根本原因

Nuxt Content 在服务端运行时会使用 SQLite 作为内容查询数据库。

如果配置为文件型 SQLite,例如:

export default defineNuxtConfig({
  content: {
    database: {
      filename: '/tmp/contents.sqlite'
    }
  }
})

那么 SSR 直接访问深层 docs 页面时,服务端需要打开或创建 SQLite 文件。

如果容器文件系统是只读的,运行时无法写入 SQLite 文件,会出现:

SQLITE_CANTOPEN: unable to open database file

最终页面可能表现为 404 Documentation page not found。

Docker 验证结论

基于当前项目用 Docker 验证了 4 种情况:

场景文件系统渲染方式直接访问 /docs/mcp-agents/setup结论
当前项目使用 prerender只读crawl prerender构建失败不能直接作为可部署方案
当前项目不使用 prerender只读SSR失败,返回 404不可行
当前项目不使用 prerender可写SSR成功,返回 200可行
当前项目给 docs 使用 prerender只读docs 静态预渲染成功,返回 200可行

验证细节

不使用 prerender + 只读文件系统

容器运行时加 --read-only,直接请求:

/docs/mcp-agents/setup

结果失败,页面返回 404 Documentation page not found,日志出现:

SQLITE_CANTOPEN: unable to open database file

说明只读文件系统会导致 Nuxt Content 的文件型 SQLite 无法打开。

不使用 prerender + 可写文件系统

同样的 SSR deep link,在容器文件系统可写时返回 200。

说明代码逻辑本身没有问题,问题点在运行时写入 SQLite 文件。

docs 使用 prerender + 只读文件系统

如果将 /docs/** 预渲染成静态 HTML,直接访问 /docs/mcp-agents/setup 返回 200。

因为页面已经在构建阶段生成,运行时不需要再查询 SQLite。

使用 crawlLinks: true 时,构建过程中确实爬到了 /docs/mcp-agents/setup,但是构建整体失败。

失败原因不是 SQLite,而是站点中存在目录分组链接:

/docs/mcp-agents

这个路径只是文档目录分组,不是实际内容页,会返回:

404 Documentation page not found

Nitro prerender 默认会因为这个 404 中断构建。

推荐方案

更稳妥的方案是把 Nuxt Content 的 SQLite 改成内存数据库:

export default defineNuxtConfig({
  content: {
    database: {
      filename: ':memory:'
    }
  }
})

这样服务端运行时不会向文件系统写入 SQLite 文件,可以适配只读容器。

对于文档量较小的站点,内存占用通常很小。当前项目文档数量不多,SQLite 内容体积也很小,使用 :memory: 比依赖可写 /tmp 或完整 prerender 更稳定。

可选方案

如果希望继续使用 prerender 解决,也可以:

  1. 给 /docs/mcp-agents 这类目录分组补一个真实首页文档。
  2. 或者配置 Nitro prerender 忽略这些目录分组 404。
  3. 确保所有需要直接访问的 docs 路由都已经被预渲染。

但这种方式依赖路由被完整覆盖,后续新增文档目录时也需要继续维护。

判断标准

  1. 线上容器是只读文件系统,并且 docs 页面需要 SSR deep link:优先使用 filename: ':memory:'。
  2. docs 页面完全静态,且所有路由都能稳定 prerender:可以使用 prerender。
  3. 文件型 SQLite 只适合运行环境允许写入目标目录的情况。