从零到上线:我的 Next.js 最小化个人博客实践
7/23/2025, 7:38:27 PM
本文记录了我从搭建最小可用博客到完成部署的全过程,包括技术选型、博客功能实现(首页、详情页、编辑页、权限管理)、图片粘贴上传、以及服务器部署与自动化构建。适合想快速实践个人博客全栈开发的人参考。
输出倒逼输入
项目之所以要开源,科研之所以要分享,是因为经得起推敲的知识才是好知识。本着这样的观念,我决定开始写博客分享我所学的知识。
博客框架还不够
借着这次机会,我决定先从个人搭建博客最小实现开始,一步步添加我需要的功能。同时练习一下我没做过的部署环节。
我是怎么做的
技术选型
框架方面就选择 next.js,成熟的 ssr 所带来的 SEO 优化和首屏加载速度提升是对我这种内容型网站有利的,同时是个全栈框架可以把我简易的后端也写在里面。剩下的工具由 chatgpt 来推荐就好,基本上都是周下载十几万以上的开源库。不用担心 bug 或者遇到问题了没地方搜和问。
以下列出最小实现博客所用到的库:
- prisma:ORM 工具
- mysql:数据库
- @uiw/react-markdown-preview:markdown 预览
- @uiw/react-md-editor:在线编辑 markdown
- remark & rehype:解析 markdown 至 html
- highlight.js:代码高亮
- gray-matter:解析 markdown 元数据
搭建博客
博客首页
目前只是调用接口后遍历渲染数据,后期可以做的漂亮一点吸引访客兴趣。
详情页
因为我们是以 markdown 格式存储的,这里用 remark 来方便地将 markdown 格式转化成对应的 html 结构,同时添加代码高亮。
const processedContent = await remark()
.use(remarkRehype)
.use(rehypeHighlight)
.use(rehypeStringify)
.process(post.content);
const contentHtml = processedContent.toString();
编辑页
以往使用博客框架的时候都是在编辑器里面写 markdown 的,这次增加一个编辑页让写作体验更完整(更多的使用 web)。
实现代码较多的是粘贴图片的功能
/*
监听粘贴事件,触发后遍历 e.clipboardData.items,
如果 item 类型为图片则用 getAsFile 获取它的文件,
调用接口上传至数据库,并把返回的 url 插入到文章中
*/
useEffect(() => {
const handler = async (e) => {
/*
clipboardEvent 虽然被 mdn 标为实验性,
但 caniuse 显示主流浏览器支持度达 98.82%
*/
if (!e.clipboardData) return
const items = e.clipboardData.items
for (const item of items) {
if (item.type.startsWith('image/')) {
const file = item.getAsFile()
if (file) {
const url = await uploadImage(file)
insertAtCursor(``)
}
}
}
}
const container = editorRef.current
container?.addEventListener('paste', handler)
return () => {
container?.removeEventListener('paste', handler)
}
}, [])
// 用 FormData 来提交二进制文件
const uploadImage = async (file) => {
const formData = new FormData()
formData.append('file', file)
const res = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
const data = await res.json()
return data.url
}
const insertAtCursor = (text) => {
const textarea = document.querySelector(
'.w-md-editor-text-input'
)
if (!textarea) return
const start = textarea.selectionStart
const end = textarea.selectionEnd
setContent((prevContent) => {
const before = prevContent.substring(0, start);
const after = prevContent.substring(end);
return before + text + after;
});
// 移动光标
requestAnimationFrame(() => {
textarea.focus()
textarea.selectionStart = textarea.selectionEnd = start + text.length
})
}
编辑页同时承担添加和修改文章的职责,所以要用 useSearchParams 加以区分,如果找到 id 就回填数据,编辑完后发送 put 请求。如果没找到 id 就显示空编辑框,写完后发送 post 请求。
权限管理
因为后台也写在项目里,所以要做权限管理。要关注的方面有:
token 签发
if (!user || !bcrypt.compareSync(password, user.password)) {
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 })
}
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{
expiresIn: '7d',
}
)
const res = NextResponse.json({ message: 'Login success' })
res.cookies.set('token', token, {
httpOnly: true,
path: '/',
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7,
})
禁止游客访达无需访达的页面
const token = request.cookies.get("token")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
try {
await jwtVerify(token, new TextEncoder().encode(process.env.JWT_SECRET));
return NextResponse.next();
} catch (err) {
return NextResponse.redirect(new URL("/login", request.url));
}
向游客隐藏他们用不到的按钮
const cookieStore = await cookies();
const token = cookieStore.get("token")?.value;
let isAuthenticated = false;
if (token) {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
isAuthenticated = !!decoded?.userId;
} catch (e) {
// token 失效或伪造
}
}
return isAuthenticated;
后端添加鉴权逻辑
const token = req.cookies.get("token")?.value;
if (!token) {
return NextResponse.json({ error: "未授权:缺少 token" }, { status: 401 });
}
try {
jwt.verify(token, process.env.JWT_SECRET);
} catch (err) {
return NextResponse.json({ error: "未授权:token 无效" }, { status: 401 });
}
到这里博客的最小实现就完成了,我可以在网页上写 markdown 语言,预览博客效果,发布到数据库,并在首页访问到。我现在就在用文章发布页写这份博客,体验很不错。
部署上线
首次部署
所用服务器系统为 Ubuntu 22.04,通过命令行来完成服务器初始化和项目部署。
# 连接至服务器
ssh root@xxx
# 更新包
apt update && apt upgrade -y
# 安装 Node.js
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
\. "$HOME/.nvm/nvm.sh"
nvm install 22.14.0
# 安装 PM2(进程管理器,保持Next.js项目常驻)
npm install -g pm2
# 安装 Nginx(反向代理服务器)
apt install -y nginx
# 安装 Git
apt install -y git
# 安装项目(记得绑定 SSH)
cd /var/www
git clone xxx
cd xxx
npm install
npx prisma migrate deploy
npx prisma generate
npm run build
# 用 pm2 来管理进程
pm2 start npm --name "my-blog" -- start
pm2 startup
pm2 save
# 配置 nginx
nano /etc/nginx/sites-available/default
# 监听 443 端口,host 匹配 server_name,
# path 匹配 location,执行对应代码块内容
# server {
# listen 80;
# server_name linhanxi.cn www.linhanxi.cn;
# return 301 https://$host$request_uri;
# }
#
# server {
# listen 443 ssl;
# server_name linhanxi.cn www.linhanxi.cn;
#
# ssl_certificate /etc/nginx/ssl/linhanxi.cn.pem;
# ssl_certificate_key /etc/nginx/ssl/linhanxi.cn.key;
#
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_ciphers HIGH:!aNULL:!MD5;
#
# location / {
# proxy_pass http://localhost:3000;
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection 'upgrade';
# proxy_set_header Host $host;
# proxy_cache_bypass $http_upgrade;
# }
# }
nginx -t # 测试配置是否正确
systemctl restart nginx
# 配置防火墙,开放 22 / 80 / 443 端口
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw enable
至此网站已经可以正常访问了!
自动化部署
上式构建项目的代码在每次项目更新后都要再执行一次,非常麻烦,让我们用 github actions + 部署脚本来实现推送代码到仓库后自动构建。
# 先在服务器写好一份构建脚本
nano /var/www/deploy.sh
# #!/bin/bash
# cd /var/www/hx-blog
# git pull
# npm install
# npx prisma migrate deploy
# npx prisma generate
# npm run build
# pm2 restart my-blog
# 赋予执行权限
chmod +x /var/www/deploy.sh
然后在项目根目录下创建 /.github/workflows/deploy.yml
name: Deploy to Aliyun
on:
push:
branches:
- master # 当 push 到 master 分支时触发
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: 连接到阿里云服务器并执行部署脚本
uses: appleboy/ssh-action@v0.1.10
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_KEY }}
script: |
bash /var/www/deploy.sh
接着在线上仓库 -> Settings -> Secrets and variables -> Actions 添加上式所需 secrets。
- SERVER_HOST:服务器 ip
- SERVER_USER:登录用户名
- SERVER_KEY:私钥
这里用到 SSH 公钥登录,所以要先在本地生成密钥后,把私钥存进 SERVER_KEY,公钥写入服务器的 ~/.ssh/authorized_keys
此时,再推送代码到线上仓库时就会自动化部署了!
我收获了什么
- 一个可以随心所欲扩建的博客;
- 理解了上线的基本流程;
写在最后
项目源码:https://github.com/hannnnxiiii/hx-blog
致谢:chatgpt —— 不求回报的后端 er