大飞的技术博客

·2 分钟

一仓四站:pnpm monorepo 里养多个官网

手上有四个网站要维护:三个公司官网加这个博客。它们技术栈相同、部署方式相同、 维护者也是同一个人,于是都住进了一个 pnpm monorepo。这篇文章讲讲这套结构 的实际运作方式,以及几个当时犹豫过的决定。

目录结构

website/
├── packages/
│   ├── www.yunjuiot.com.cn/   # 公司主站(端口 3000)
│   ├── www.elinchong.com/     # 品牌站(端口 3001)
│   ├── www.xunhong168.com/    # 单页官网(端口 3002)
│   └── www.shenyifei.com/     # 这个博客(端口 3003)
├── shared/                    # 跨包共享资产(pcb3d 流水线等)
├── scripts/                   # OG 封面、PDF 等 Python 生成脚本
└── docs/plans/                # 每个功能的设计文档

每个站点是一个独立的 Next.js App Router 包,有自己的 package.jsonnext.config.ts,互不引用对方的组件。共享的只有根目录的依赖声明、 生成脚本和资产——组件级别的「复用」我刻意没做,后面说为什么。

依赖:全在根上

站点级 package.json 只声明 scripts,依赖全部挂在仓库根:

{
  "name": "www.shenyifei.com",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --turbopack -p 3003",
    "build": "next build"
  }
}

四个站共用一个 Next.js 版本,升级时一次升级全部,不存在「A 站还在 Next 14、B 站已经 Next 15」的漂移。代价是升级需要四站都回归一遍, 但静态导出站点的回归成本低(构建过 = 大概率没事)。

约定大于配置

同一件事在四个站里长得一模一样:

  • output: 'export' 静态导出,images: { unoptimized: true };
  • app/layout.tsx 里集中管理 SEO 元数据、OG 封面、结构化数据;
  • sitemap.ts / robots.ts / not-found.tsx 三个基础设施文件齐全;
  • 端口号写死在 dev script 里,3000 起,一个站一格。

约定带来的好处在改版时最明显:我知道任何一个文件的「对应物」在另外 三个站的哪个位置,AI 辅助改版时提示词都省了。

为什么不做组件级共享

最早考虑过把 Header/Footer/ShareBar 抽成 workspace 共享包,后来放弃了:

  1. 四个站的品牌视觉完全不同,能共享的只有逻辑骨架,而骨架只有几十行;
  2. 抽象包一旦改坏,四个站同时挂,耦合的爆炸半径反而变大;
  3. 每个站代码量都不大(几百行),复用收益撑不起一层抽象的维护成本。

真正值得共享的是资产和流水线:PCB 的 STEP→GLB 转换脚本、OG 封面 生成脚本,这些放 shared/scripts/,谁用谁调,不产生运行时耦合。

一个 pnpm 特有的坑

pnpm 的 node_modules 是绝对路径 junction,某个包里有 node_modules 时 (比如 shared/pcb3d 自带依赖),webpack 的 FileSystemInfo 快照会把它 误处理,构建直接崩:

// next.config.ts — 按语义并入 managedPaths 跳过快照
config.snapshot.managedPaths = [
  ...(config.snapshot.managedPaths ?? []),
  /[\\/]scripts[\\/]pcb3d[\\/]node_modules/,
];

这类问题没有通用解,记住 pnpm 的 monorepo 里出现「莫名其妙的构建崩溃」, 先怀疑 junction 路径。

小结

monorepo 对「多个小站、一个人维护」的场景是明确的正解:依赖统一、 约定统一、部署统一。但要忍住两个冲动——不要过早抽共享组件包, 也不要为了让目录好看而引入构建复杂度。