我把跑了五年的旧博客升级到了 3.0
一篇关于把自己跑了五年的旧博客,重构成 Next.js 全栈新版本的折腾记录。涉及 2G 内存阿里云 ECS、宝塔多站点共存、Let's Encrypt 自动续期、国内 GitHub OAuth 抽风、数据零丢失迁移…如果你也在折腾自己的小站,希望对你有点参考。
一、为什么要折腾
我的博客 mangoya.cn 跑在阿里云 ECS 上已经五年多了,2.0 是 2021 年用 Vue 2 + Element UI + Koa + MongoDB 写的,分成三个独立仓库:前台、后台、服务端。
后来就懒了。最后一篇文章停在 2024-04,整套技术栈也明显过时:
- Vue 2 已于 2023 年底官方 EOL,
vue-cli也不再维护 - 前台是纯 CSR,搜索引擎抓不到内容,SEO 几乎为零
- 三个独立仓库,前后台技术栈割裂(一个 Vue 2,一个 Vue 3 + Vite,一个 Koa)
axios 0.19有已知 CVE,tslint早就废了- Mongoose 5 在 Node 20+ 上根本起不来
最致命的是:HTTPS 证书过期了 9 个多月,自动续期也挂了。我每次想打开自己的博客都得先点"忽略证书警告",太尴尬。
干脆推倒重做一版 3.0。
二、技术选型
| 层 | 旧版 (2.0) | 新版 (3.0) |
|---|---|---|
| 前台 | Vue 2 + vue-cli + Element UI(CSR) | Next.js 15 (App Router) + React 19 + TS(SSR) |
| 后台 | Vue 3 + Vite + Element Plus(独立仓库) | 同一个 Next 应用 /admin 路径,middleware 鉴权 |
| 服务端 | Koa 2 + TS + Mongoose 5(独立仓库) | Next.js Route Handlers + Mongoose 8(BFF 内置) |
| 样式 | less + Element UI 主题 | Tailwind CSS + 自定义 Butterfly 风 |
| 鉴权 | 自管 JWT + 微博/GitHub OAuth | Auth.js v5 + GitHub OAuth |
| 部署 | PM2 + nginx 静态托管 | Docker 容器 + 宝塔 nginx 反代 |
核心理念:三个仓库合成一个 Next.js 应用。前台是 Server Component 直接查库(SSR + 自带 SEO),后台是 /admin 路径 + middleware 鉴权,BFF 是 Route Handlers。需要的代码全部塞进一个 monorepo?不,对个人博客这是过度设计,一个干净的 Next 仓库就够了。
三、几个让我多写了好多行代码的"坑"
1. 2G 内存的 ECS 跑得动 Next.js 吗?
我这台 ECS 是 2 vCPU / 2 GiB,CentOS 7.9,跑着宝塔面板和十几个其他站点(朋友的、自己的小项目、Outline 服务端…)。free -h 一看,available 只剩 357MB。
docker build Next.js 的内存峰值动不动就 2-4G,在这台机器上构建必 OOM。
最终方案是「外部构建,本地运行」:
# 我的开发机(247G 内存的怪物)
docker build -t myblog3:tag .
docker save myblog3:tag | gzip > tag.tar.gz # 70MB
# scp 到 ECS
docker load < tag.tar.gz
docker run -d --network=host --memory=350m ...
实测 Next.js standalone 运行时只占 80MB 内存,加上限制 350M 上限,完全够用。整套封装成一个 deploy.sh,以后部署一条命令。
2. 怎么和宝塔上十几个站点和平共存?
宝塔的 nginx 是它在管,里面有 PHP fpm 配置、伪静态规则、十几个 server_name。我不能直接 docker compose 抢 80/443 端口,那会把人家的站点全搞挂。
方案:
- Next 容器只监听
127.0.0.1:3000(内网,不暴露公网) - 在宝塔 nginx 的
mangoya.cn.conf里把location /改成proxy_pass http://127.0.0.1:3000 - 保留 SSL、
well-known验证路径不动 - 删掉原来
root /home/www/Aimee/myblog2.0/dist;和静态文件缓存的 location(那些.js .css .png的缓存规则会拦截 Next 的_next/static/*让它们 404)
切换瞬间没有掉线(reload 平滑),其他十几个站点完全没受影响。
3. 数据怎么零丢失迁移?
旧库 aimeeblog 里有 52 篇文章、880 条评论、627 条留言、64 个用户、5857 个点赞,全是真实积累,绝不能丢。
我做了一件最重要的事:不动数据,只动代码。
新版的 Mongoose model 字段、collection 名(article、comment、articleCate、tags、user…)完全对齐旧 schema:
const ArticleSchema = new Schema<IArticle>(
{
title: { type: String, required: true },
content: { type: String },
classId: { type: String },
tags: { type: [String], default: [] },
// ...
},
{ collection: 'article' }, // 显式指定,不要让 mongoose 自动复数
);
新代码上线,零迁移、零转换,直接连旧库读写。Mongoose 8 兼容 MongoDB 4.4,连版本都不用升。
4. 国内服务器搞 GitHub OAuth 是会抽风的
GitHub 登录流程里有一步是服务器主动访问 github.com/login/oauth/access_token 拿 token。我刚上线时几次都 fail:
[auth][error] CallbackRouteError: fetch failed
[auth][cause] TypeError: fetch failed (SocketError UND_ERR_SOCKET)
remoteAddress: 20.205.243.166, bytesWritten: 597, bytesRead: 0
ClientHello 发出去了,没收到回应——典型的 SNI 干扰。诡异的是:
api.github.com一直稳定通(user 信息接口走这里)github.com间歇性 reset(token 接口走这里,正好被打到)
宿主机 curl 测 5 次都通,但容器里 Node 偶尔翻车。等不到 GFW 心情好,只能加自动重试兜底——给 Node 全局 fetch 注入 undici 的 retry 拦截器:
// instrumentation.ts (Next.js 启动钩子)
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { setGlobalDispatcher, Agent, interceptors } = await import('undici');
setGlobalDispatcher(
new Agent({ connect: { timeout: 10_000 } }).compose(
interceptors.retry({
maxRetries: 4,
minTimeout: 300,
maxTimeout: 2_000,
errorCodes: ['UND_ERR_SOCKET', 'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT'],
}),
),
);
}
}
⚠️ 一个小坑:
undici@8调了 Node 21+ 才有的util.markAsUncloneable,在node:20-alpine上加载直接抛错。降到undici@6就好了。
加上之后再没失败过。
5. 证书过期了 9 个月
旧证书是 DigiCert 免费 DV,2025-08-09 就到期了,自动续期早不知道哪天挂的。
新版直接换 Let's Encrypt,用 acme.sh 接管:
acme.sh --issue -d mangoya.cn -d www.mangoya.cn -w /www/wwwroot/acme-challenge \
--server letsencrypt
acme.sh --install-cert -d mangoya.cn --ecc \
--key-file /www/server/panel/vhost/cert/mangoya.cn/privkey.pem \
--fullchain-file /www/server/panel/vhost/cert/mangoya.cn/fullchain.pem \
--reloadcmd "nginx -s reload"
--install-cert 不止安装当前证书,还把续期后的「reload 命令」记录到 acme.sh 的续期任务里——以后续期完它自己装回宝塔的证书路径并 reload nginx。自动续期 + 永不过期 + 全免费。
中间还闹了个乌龙:我改 nginx 反代时不小心删掉了 .well-known/acme-challenge 的 root,导致验证路径返回 404,acme.sh 第一次签发失败。修回去,单独给它一个 webroot:
location ^~ /.well-known/acme-challenge/ {
root /www/wwwroot/acme-challenge;
default_type text/plain;
allow all;
}
记住:^~ 前缀优先级高于正则 location,能挡住后面的反代 location /。
四、UI 没换风格,复刻了 2.0
我之前博客是 Butterfly 风格——彩色波浪渐变背景、顶部大图 banner + 打字机文字、左主右栏布局、卡片左上角圆形日期角标、左突出带小三角的分类标签、Do you like me 心形雪碧图、返回顶部小猫…
新版我完整复刻:
evanyou.js那个 canvas 三角波浪背景,移植成了 React Hook- 打字机用
useEffect重写(踩了一个 React 经典坑:phrases默认参数每次渲染都是新数组引用 →useEffect依赖检测到"变化"→ 定时器反复重启,打字机只能打两个字母就回头。把默认值提成模块常量就好了) - 心形雪碧图:
background-position: 0 → -2800px+transition: 1s steps(28),原汁原味 - OwO 表情:旧版 72 个表情包,全部搬过来,发评论用
[微笑]这种语法
整体视觉跟 2.0 一模一样,但底下是现代化的 Next.js + Tailwind。
五、顺手把简书外链图迁移了
老文章正文里有 25 张简书图床的图片(upload-images.jianshu.io),随时可能挂——简书有防盗链历史。
写了一个迁移脚本:扫 article.content 里的简书 URL → 下载到 public/migrated/<hash>.<ext> → 替换 markdown 里的 URL → 更新 mongo。
const articles = await ArticleModel.find({
content: /upload-images\.jianshu\.io/
}).lean();
for (const a of articles) {
const urls = a.content.match(/https?:\/\/upload-images\.jianshu\.io\/[^\s)"'<>]+/g) ?? [];
// 下载、转存、替换、updateOne...
}
跑完 25 张图全本地化,再不怕简书哪天关停了。
六、最后看一眼
✅ Next.js 15 + React 19 + TypeScript + Tailwind
✅ 单仓 monorepo(前台 + 后台 + BFF 一个应用)
✅ 数据零丢失复用(52 文章 / 880 评论 / 627 留言 / 64 用户)
✅ Docker 容器化部署,与现网 10+ 站点零冲突
✅ HTTPS Let's Encrypt 自动续期
✅ GitHub OAuth 登录(undici 重试加固,国内可用)
✅ Butterfly 风格 UI 100% 复刻
✅ 镜像 70MB,运行时内存 80MB
整个工程大概花了一个晚上的核心时间,复杂的不是某一个技术点,是怎么不打扰别人:不要碰宝塔在管的其它站点、不要碰朋友的 docker 容器、不要让数据迁移出岔子、不要让现网服务断哪怕一秒。
旧 PM2 服务、旧前台静态目录都已经下线,腾出来 ~160MB 内存。
代码已经开源在 github.com/Aimee1608/myblog3.0,从 PLAN.md 到 deploy.sh 全在那里。
写完发现:把一个老站重构成「现代但风格不变」,意外比从零开始还有意思。老博客就像老房子,能不能装修不动结构、不影响邻居、还能保留所有照片,才是真功夫。
如果你的小站也在「想升级又不敢动」的状态,欢迎留言聊聊。
评论(0)
登录后参与评论。
还没有评论,来抢沙发吧。

