2025年8月15日

Mind.com 网站是如何构建的:Nuxt 4 架构解析

Mind.com 营销网站的技术详解 — Nuxt 4 SSR、@nuxt/content、i18n、Nitro 服务器路由,以及每个选择背后的权衡。

Mind.com 网站是如何构建的:Nuxt 4 架构解析

这将详细介绍 mind.com 网站的实际构建方式,以及我们做出这些选择的原因。它是一个营销网站——包括着陆页、博客、法律页面和联系表单——因此有趣之处不在于任何单一技术。它在于我们划定的界限:哪些内容在服务器上渲染,哪些发送到浏览器,哪些作为构建时脚本保留。如果某个决定是权衡而非显而易见的优势,我们也会明确指出。

该网站是 pnpm monorepo 中的一个包(仓库名为 InterMIND)。它运行在 Nuxt 4 上,并部署到 Vercel。这里没有任何奇特之处;其价值在于各部分之间的契合度。

为什么选择带 SSR 的 Nuxt 4 而非静态构建

营销网站可以是纯静态输出。然而,我们使用 Nuxt 的 Nitro 引擎和 vercel 预设在服务器端进行渲染,其原因具体而实际,并非仅仅出于愿景。

我们有一个提交到后端的联系表单,跨七种语言的每区域路由,以及频繁更新的内容,使我们无需考虑哪些页面可能已过期。SSR 允许一个代码库处理营销页面、本地化路由和表单端点,而无需在静态捆绑包上附加独立服务。Nitro 在构建时将服务器编译成 Vercel 函数,因此我们无需运行或维护自己的服务器即可获得服务器渲染。

权衡是坦诚的:SSR 意味着有请求时的工作,而纯静态网站则没有。对于这种规模的网站,其成本很小,作为交换,我们避免了将静态前端与独立 API 拼接在一起的协调问题。如果这是一个拥有 10,000 页且没有动态表面的文档网站,静态生成会是更好的选择。但事实并非如此,因此 SSR 胜出。

内容:@nuxt/content 和 MDC

博客和法律内容是 content/blog/content/legal/ 下的 Markdown 文件。我们使用 @nuxt/content v3,它在构建时将这些文件解析到本地的 better-sqlite3 数据库中,并允许我们像查询数据一样查询它们。通过 content.config.ts 中的 Zod schema 强制执行 Frontmatter 结构,因此缺少 titledate 格式不正确的帖子将导致构建失败,而不是发布损坏的内容。

博客以 MDC 编写——带组件的 Markdown。提示框是 :::tip{title="..."} 而非原始 HTML,这使得源代码更具可读性,并允许我们在一个地方(ProseTip 组件)控制渲染,而不是将标记分散到每个帖子中。

将内容作为文件保留在仓库中意味着它受到版本控制,可以在拉取请求中进行审查,并且可进行差异比较。无需登录独立的 CMS,也无需备份数据库。其局限性也随之而来:编辑需要提交,因此这适合已经习惯 Git 的团队,而不适合庞大的非技术编辑人员。

国际化及翻译工作方式

网站通过 @nuxtjs/i18n 采用 prefix_except_default 策略,支持七种语言环境:英语、西班牙语、葡萄牙语、法语、德语、俄语和中文。英语从根目录提供服务;其他每种语言环境都位于其自己的路径前缀下。UI 字符串位于 app/locales/<code>.json 中。

翻译是一项双轨工作,我们将其分开进行。UI 字符串和内容通过两个 Node 脚本——scripts/i18n-translate-ui.tsscripts/i18n-translate-content.ts——进行翻译,这两个脚本基于 AI SDK 和 Anthropic 模型构建。它们作为构建时创作步骤运行,而非请求时:人类触发它们,审查输出,并提交。翻译结果是仓库中的普通文件,与其他所有文件一样。

这是我们刻意划定的界限。没有运行时翻译,也没有访客加载页面时的即时语言模型调用。翻译提前生成并作为普通的本地化路由提供,这使得渲染可预测,并允许我们在任何内容上线前进行校对。

设计系统:Tailwind v4 和 @nuxt/ui

样式采用 Tailwind CSS v4,并在此基础上使用 @nuxt/ui v4。新访客默认进入深色模式;页面背景特意选择接近纯黑的 #0a0b0d,而非纯黑色。每个页面和组件都设计为在亮色和深色模式下均可正常工作——这是我们坚守的约束,而非事后考虑,因此没有硬编码任何仅适用于亮色模式的颜色。

依靠 @nuxt/ui 意味着我们继承了可访问、一致的组件,而无需从头开始重建按钮、表单控件和叠加层。代价是我们要遵循一个依赖项的约定,而不是一个完全定制的系统,但对于营销网站来说,这是值得的权衡。

后端:Nitro 服务器路由和 Pipedrive

后端规模很小,位于同一项目中,作为 server/ 下的 Nitro 服务器路由:

  • server/api/submit-form.post.ts 处理联系表单。
  • server/api/health.get.ts 是健康检查。
  • server/api/__sitemap__/urls.ts 为站点地图提供数据。
  • server/routes/llms.txt.tsllms-full.txt.ts 为 AI 爬虫提供机器可读的摘要。
  • server/middleware/ 包含横切逻辑,包括用于已停用 URL 的 410-gone 处理程序和受众处理。

当有人提交联系表单时,潜在客户会通过 server/utils/pipedrive.ts 发送至 Pipedrive,该脚本会创建一个人和一条潜在客户记录。这是唯一的 CRM 集成,也是表单数据唯一的去向。中间没有队列、数据管道或对象存储——端点验证输入并调用一个 API。保持如此直接意味着几乎没有什么会出错,即使出错也无需过多推敲。

此外,营销网站上故意没有用户身份验证。它是一个宣传册和联系表单,而不是一个应用程序;增加登录和会话会增加我们需要保护的攻击面,却没有任何好处。

可观测性:Sentry 和 PostHog,附带同意门控

我们运行两个可观测性工具,它们各司其职。

Sentry (@sentry/nuxt) 用于捕获错误。它作为构建时模块集成,在开发环境中保持关闭,以避免本地噪音。它的职责是在生产环境中出现问题时通知我们。

PostHog (nuxt-posthog) 处理产品分析,其围绕同意的行为是值得详细说明的部分。它默认是选择退出状态。在 Usercentrics 同意平台授予权限之前,不会捕获任何数据,此时 app/plugins/posthog-consent.client.ts 会启用它。默认是不进行跟踪;同意才会将其开启,而不是相反。PostHog 负责分析,Sentry 负责错误——这就是全貌,没有额外的标签管理器或第三方分析层。

SEO 和 AI 爬虫

SEO 通过 @nuxtjs/sitemap@nuxtjs/robots 处理站点地图和 robots 指令。重定向和缓存头——包括网站积累的大量旧 URL 重定向——位于 vercel.json 中,紧邻它们在边缘生效的位置。

上述提到的 llms.txtllms-full.txt 路由是对当前网络阅读方式的一种致敬:它们为 AI 爬虫提供了网站的清晰、结构化摘要,而不是让它们抓取渲染后的页面。这种方式服务成本低廉,意味着读取网站的机器能够获得准确的版本。

部署

一切都部署到 Vercel。Nitro 的 vercel 预设将服务器路由转换为 Vercel 函数,并将页面转换为服务器渲染输出,因此 git push 就会变成一次部署,无需维护单独的构建和发布管道。作为 pnpm monorepo 的一部分,mind.com 与其他包共享工具和锁文件管理,同时保持独立部署。

总结

所有这些都不是什么稀奇事,而这正是重点。Nuxt 4 让我们无需运行服务器即可实现 SSR;@nuxt/content 将帖子和法律页面作为可审查的文件保留;i18n 和翻译脚本将网站本地化为我们可以在上线前校对的构建步骤;精简的 Nitro 后端将潜在客户交给 Pipedrive;Sentry 和 PostHog 监控故障和行为,分析功能在获得同意前保持关闭。

网站之所以易于理解,是因为我们尽可能减少了可变部分,并将每个部分都放置在适当的位置。对于营销网站来说,平实易懂永远胜过巧妙复杂。

← 所有博文