|

Aimee

Write the Code. Change the World.

我把跑了五年的旧博客升级到了 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 年底官方 EOLvue-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 OAuthAuth.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 名(articlecommentarticleCatetagsuser…)完全对齐旧 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

登录后参与评论。

还没有评论,来抢沙发吧。

回到顶部