构建实录 / 2026-06-27
我怎么用 AI 把个人站从 SPA 迁到 Next.js,又迁到 Cloudflare Pages

这个站你正在读的这套博客系统,经历了两次迁移:一次从 Vite SPA 到 Next.js App Router(为了能做博客),一次从服务器 Node 到 Cloudflare Pages 静态导出(为了甩开服务器)。两次都是我用 Claude Code 做的,我的角色更像产品经理:提需求、拍板、验收。
现状先摆出来:Next.js App Router、全站静态生成、84 个静态页、13 篇文章、13 个项目,一套 14 个校验脚本 + smoke + GitHub Actions 守着,push 到 GitHub 就自动部署。这篇讲它是怎么长成这样的,包括一个把我坑得不轻的部署认知错误。
第一次迁移:为什么 SPA 做不了博客
站最早是 React 19 + Vite 7 的纯前端 SPA,视觉做得很足——光标跟随光、hero 涟漪、滚动渐显、头像视差。
但它做不了博客,两个硬伤:纯客户端渲染,爬虫抓到的基本是空壳,文章写了搜索引擎收不进去;每篇文章没法有独立 OG 分享卡,发到 X 上就是一条光秃秃的链接。对一个想靠内容沉淀的个人站,这两条恰恰是命根子——文章要能被搜到、被分享,博客才成立。所以迁到 Next.js App Router,全站静态生成。
迁移的纪律是"能不重做就不重做":旧站的设计、文案、数据能平移的全平移,只动非动不可的。三个 CSS 文件原样搬,视觉零损失;动效 hooks 全留着,只改一处——Next 是文件路由,页面切换后 DOM 换了,动效要重绑目标元素,给几个 hook 加了 routeKey 依赖,路由变了就重跑;projectCatalog.js 一行没改。真正重写的只有路由:手写的 pushState 换成 Next 文件路由,[lang] 动态段撑起中英双语。
预构建 JSON:绕开 Serverless 读不到文件的坑
写博客系统前,我看了一篇别人的建站复盘(JasonZhu.AI),把踩过的坑列得很清楚,等于一份提前避雷的清单。
最关键一条是 Serverless 读不到文件:用 fs.readdirSync 读 Markdown,部署到 Serverless 上找不到文件——md 没被打包进去,运行时文件系统里根本没有。
解法我直接抄了,也成了这个站的核心模式:构建前跑脚本,把 md 全读出来生成 posts.json,页面直接 import 这个 JSON,运行时不碰文件系统。 这个模式后来价值远超预期——它意味着整个站可以纯静态导出(见下文第二次迁移),因为没有任何一处依赖运行时文件系统。写文章的日常变成:写 md、push、自动上线。
清单里另外几个坑也顺手绕了:环境变量末尾换行符会让密码校验莫名 401,用前 trim;frontmatter 标题带中文引号会让 YAML 崩,改用「」;带方括号的路径在 shell 里要引号包起来([lang] 目录尤其容易中招)。
第二次迁移:从服务器 Node 到 Cloudflare Pages 静态导出
第一次迁移后,站部署在自己的服务器上跑 Node 进程 + nginx 反代。这次要把它迁到 Cloudflare Pages 静态托管——不碰服务器,push 就自动部署。
这次迁移的起点是一个认知错误,值得记一笔:有阵子我改了内容 push 上去,线上却一直是旧的。我一开始以为是缓存,后来才查清——站根本不是自动部署的,是部署在自建服务器上、需要手动拉代码构建重启。我此前一直默认"push = 上线",但那台服务器没有任何流程监听 GitHub。 教训:搞清楚"部署到底怎么发生",比写代码本身更早该做。
查清后决定干脆迁到 Cloudflare Pages。这个站是全 SSG 的,本该完美适配静态托管,但有三个技术点要处理:
1. output: "export" + 删掉 middleware。 静态导出不支持 middleware。原来有个 middleware 做本地化 404(未知路径返回带 noindex 的双语 404 页)。静态导出下改由 app/not-found.jsx 生成 404.html,Cloudflare Pages 对任何未匹配路径自动返回它——功能等价,精细度降一档(不再逐路由判断),公开站够用。
2. redirects/headers 迁到托管层。 next.config 的 redirects()/headers() 在 export 模式失效。/→/zh 用 public/_redirects,安全头(nosniff/DENY/Referrer-Policy)用 public/_headers——Cloudflare Pages 原生认这两个文件。
3. 干净环境构建验证。 Cloudflare 是 git clone + npm ci 的干净环境,我先在本地裸 clone 一份模拟它跑 npm run build,确认那 14 个 prebuild 校验脚本在没有本地文件的环境下也能过——这一步排掉了"本地能构建、CF 上挂"的最大风险。
Pages 项目连 GitHub 后,build 命令 npm run build、输出 out、环境变量 NODE_VERSION=22(默认 Node 版本对 Next 16 太旧会挂)。绑 custom domain 时 Cloudflare 自动把 DNS 从服务器 A 记录切成 Pages——顺带一个坑:旧 nginx 发过 s-maxage 一年的缓存头,切完要 Purge 一次缓存,否则边缘还押着旧页面。
一套校验守着"push 即上线"
push 即上线的前提是 push 的东西不会坏。所以 prebuild 挂了一串校验:catalog:check(项目目录一致)、links:check(外链社交入口)、posts:check(文章 frontmatter、图片、残留标记)、materials:check(材料与 proof 资产),过了才生成 posts.json、feeds、discovery 产物。加上 smoke-site.mjs 点核心页面、GitHub Actions 在 push/PR 再跑一遍。这套东西让"加页面、加文章"变成安全操作——每次都知道有没有把已经好的入口弄坏。
两次迁移的共同感受
框架级迁移——换路由、换渲染模型、换托管平台——放几年前是要专门排期的活。现在两次都是一个会话量级的事:第一次一个下午从"决定迁"到"骨架跑通、build 出静态页";第二次半天从"改 output:export"到"生产域名切到 Pages"。
成本低到这个程度,"要不要迁"就不再是技术能不能做的问题,而是想不想做的问题。而那个部署认知错误也提醒我:AI 能飞快地帮你改代码,但"系统到底怎么运行的"这种事实,得你自己去核实——工具跑得快,不代表你可以不知道路在哪。
你正在读的这篇文章,就跑在它自己讲的这套流程上:一份 md、一次 push、Cloudflare 自动构建、静态页上线。