从零搭建个人博客:手把手完整教程与踩坑记录
从零构建个人博客的完整流程被一步步拆解:配合 Node ≥22、pnpm、Git,使用 Astro+Firefly 主题和 GitHub Actions 自动构建,服务器仅托管静态文件,部署在国内云服务器配 Nginx。文中详细列出环境准备、项目初始化、目录与配置结构、主题自定义、写作与发布流程,以及三大常见坑(URL 斜杠、浏览器缓存、Vite 监听异常)的解决办法。服务器配置涵盖安全组放行、SSH 免密登录与防火墙注意点,最后演示从本地构建到全自动上云的发布链。
这篇文章把我从零搭建这个博客的全过程记录下来:每一步怎么操作、用什么命令、为什么这么做、踩过哪些坑。照着做就能复现出一模一样的系统,也当作我自己的运维手册。
日常使用(写文章、发动态)在《博客日常使用指南》,这篇只讲「怎么把站搭起来、怎么自动化」。
全文涉及敏感信息的地方(服务器 IP、密码等)都用
<占位符>代替,实际操作时替换成你自己的。
一、整体规划
先想清楚要什么,再动手:
- 静态博客:文章是 Markdown,构建后是纯 HTML。没有数据库、没有后端、没有登录入口,黑不掉也慢不了,维护成本约等于零;
- 国内云服务器托管:文章推到 GitHub 仓库,GitHub Actions 构建完成后,服务器自动拉取构建产物上线,服务器只负责 Nginx 托管静态文件;
- 全自动发布:
git push之后构建、分发、上线全部自动,我只负责写文章。
技术栈一句话:Astro + Firefly 主题 + GitHub Actions + Git + Nginx。
二、环境准备
我的本地环境是 Windows + PowerShell。需要装三样东西,每样都有验证方法。
1. Node.js(≥ 22)
去 Node.js 官网 下载 LTS 安装包,一路下一步。装完重开一个终端验证:
node -v # 输出 v22.x.x 即可npm -v老版本不够就先卸载再装新版,Node 多版本共存工具(nvm-windows)是进阶玩法,入门没必要。
2. pnpm(≥ 11)
Node 自带 npm,用它装 pnpm:
npm install -g pnpmpnpm -v坑点:如果项目用
.npmrc锁定了 pnpm 版本,直接运行pnpm时它会自动切换到项目要求的版本,不用手动降级。
3. Git
去 git-scm.com 下载安装,安装选项保持默认即可。第一次用 Git 需要配置身份(每条提交记录的署名):
git config --global user.name "你的用户名"git config --global user.email "你的邮箱"坑点:Windows 下有些终端装完 Git 后找不到命令,重开一个终端窗口就好;还不行就把 Git 的
cmd目录加进PATH环境变量。
三、初始化项目
1. 拿到主题代码
在 GitHub 上把 Firefly 主题仓库 Fork(或右上角 Use this template)到自己账号下,然后克隆到本地:
git clone https://github.com/<你的用户名>/<仓库名>.gitcd <仓库名>建议直接在 GitHub 上把仓库设为 Private——文章草稿、服务器配置这些不需要公开。
2. 安装依赖并启动
pnpm install # 第一次运行,下载所有依赖,可能要几分钟pnpm dev # 启动本地开发服务器浏览器打开 http://localhost:4321,看到主题默认首页就成功了。dev server 支持热更新:改代码保存,页面自动刷新。
3. 认识目录结构
几千个文件里,你日常只需要碰这几处:
├── astro.config.mjs # Astro 全局配置(构建、插件、trailingSlash 等)├── src/│ ├── config/│ │ ├── index.ts # ★ 站点所有配置都在这一个文件(按模块分区编号)│ │ └── FooterConfig.html # 页脚文字(唯一的例外)│ ├── content/│ │ ├── posts/ # ★ 文章(Markdown,按分类建文件夹)│ │ ├── dynamic/ # 动态(短内容随手记)│ │ └── spec/ # 关于页/友链页等固定页面│ └── styles/ # 样式微调└── public/ # 纯静态资源(favicon、壁纸、相册图片)记住一条:想改任何功能和文案,先打开 src/config/index.ts 找编号注释,九成能找到。
四、站点配置
配置都是改 TypeScript 文件,保存即时生效。所有配置集中在 src/config/index.ts,按模块编号分区。
1. 站点基本信息(① 站点基础模块)
// 站点名称:站点信息、导航栏标题、首页横幅共用这一处const SITE_TITLE = "你的博客名";
export const siteConfig: SiteConfig = { title: SITE_TITLE, subtitle: "一句话简介", site_url: "https://你的域名.com", // 有域名填域名,暂时没有可以先填 IP 方案 themeColor: { hue: 220, // 0-360 色相,一个数字全站变色(220 是蓝色) defaultMode: "light", }, siteStartDate: "2026-09-20", // 建站日期,页脚的运行天数从这里算 // ...};pages 字段是各功能页的总闸,用不到的设为 false——导航、sitemap 里会自动消失,不用到处删链接:
const pages = resolvePageToggles({ friends: true, // 友链 dynamic: true, // 动态 gallery: true, // 相册 booknav: true, // 书签导航 bilibili: true, // 追番});2. 作者信息(② 用户资料模块)
头像图片放进 src/assets/images/,然后在配置里引用:
export const profileConfig: ProfileConfig = { avatar: "assets/images/avatar.png", name: "你的昵称", bio: "一句话介绍自己", links: [ // 图标用 Iconify 格式,showName: false 只显示图标 { name: "QQ群", icon: "fa7-brands:qq", url: "https://qm.qq.com/xxx", showName: false }, { name: "Email", icon: "fa7-solid:envelope", url: "mailto:你的邮箱" }, ],};这份 links 会同时用在侧边栏资料卡和首页横幅上,只改这一处。
3. 其他常用模块
配置文件里每个模块都有中文注释,改之前读一遍注释就够了。我启用了几个值得一说的:
- AI 总结(⑩):构建时调用免费 AI 接口给每篇文章生成摘要,无需密钥,参数都在注释里;
- 随机封面(⑪):文章不写
image字段就自动配随机二次元封面,图源是按顺序备用的公共 API 列表; - 追番页(Bilibili UID):
siteConfig的bilibili.uid填自己的 B 站 UID,追番进度自动同步。
4. 写一篇文章试手
文章就是 src/content/posts/ 下的 Markdown 文件,头部 frontmatter 是元信息:
---title: 我的第一篇测试文章published: 2026-09-21description: 摘要,会显示在列表页tags: [标签1, 标签2]category: 技术draft: falseslug: my-first-post # 不填则用「文件夹+文件名」当 URL---
正文用 Markdown 写……保存后回浏览器看效果。完整的逐字段说明、正文语法、发布流程都在使用指南里,这里不重复。
五、踩坑记录
搭的过程中遇到的三个印象最深的坑,写下来防止后人(和未来的我)重踩。
坑 1:不带斜杠的 URL 404
Astro 配置了 trailingSlash: "always" 后,/posts/xxx(无斜杠)直接 404。而生产环境静态托管对无斜杠请求一般会自动 301 补斜杠,本地却不行,行为不一致。
解决:把配置改成 "ignore",再加一个中间件统一重定向:
import { defineMiddleware } from "astro:middleware";
export const onRequest = defineMiddleware((context, next) => { const parsed = new URL(context.request.url); const { pathname } = parsed; const last = pathname.split("/").pop() ?? ""; // 跳过:根路径 / 已带斜杠 / 带扩展名的文件 / Astro 与 Vite 内部端点 if ( pathname === "/" || pathname.endsWith("/") || last.includes(".") || pathname.startsWith("/@") || pathname.startsWith("/_") ) return next(); return context.redirect(pathname + "/" + parsed.search, import.meta.env.DEV ? 302 : 301);});坑中坑:重定向状态码在开发环境要用 302,别用 301——301 会被浏览器永久缓存,万一重定向规则写错了,之后怎么改都跳旧地址,只能清缓存。
坑 2:主题色改了但浏览器没变
改了色相值刷新还是旧颜色,其实是浏览器 localStorage 缓存了旧值(主题把用户的色相选择存在里面)。清站点数据或 Ctrl+F5 强刷即可。不是代码的问题。
坑 3:dev server 莫名崩溃
Windows 上有个系统临时文件在盘符根目录,被 Vite 的文件监听扫到时会抛 EINVAL 直接把 dev server 搞崩。
解决两步走:在 astro.config.mjs 里把该文件加入 server.watch.ignored 忽略列表(治本),再写个循环脚本崩溃后自动重启(兜底):
while ($true) { pnpm dev; Start-Sleep -Seconds 3 }教训:玄学崩溃先看完整报错栈,定位到具体文件路径,别急着怀疑自己的代码。
六、服务器选购与初始化
1. 配置选择
静态博客对配置要求极低,我选的是国内云厂商的入门款:
| 项 | 配置 | 说明 |
|---|---|---|
| CPU/内存 | 2核 2G | 纯静态托管绰绰有余 |
| 带宽 | 固定 3 Mbps | 个人博客流量足够,以后可套 CDN |
| 系统盘 | 40G SSD | 用不满 |
| 系统 | Ubuntu 26.04 LTS | 生态最省心 |
| 价格 | 百元级/年 | 新人活动价 |
注意:国内服务器绑定域名需要 ICP 备案,备案期间可以先用 IP 访问调试(备案只管域名,IP 直连不受影响)。
买完第一件事:在控制台重置服务器密码,记下来,下面 SSH 要用。
2. 放行端口
云服务器默认只开 SSH(22)。去控制台 安全组 → 入方向 → 添加规则,放行:
80/80(HTTP)443/443(HTTPS,以后用)
协议 TCP,源 0.0.0.0/0。
坑点:服务器内装好一切却访问不了,八成是安全组没放行——在服务器里 curl 是通的,外面就是连不上。系统自带的防火墙(ufw)一般是关闭的,问题基本都出在云平台的安全组这一层。
3. SSH 免密登录
每次输密码太麻烦,配一下密钥登录:
# 本地生成密钥对(一路回车)ssh-keygen -t ed25519
# 把公钥装到服务器上(这一步需要输一次密码)ssh <用户名>@<服务器IP> "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" < ~/.ssh/id_ed25519.pub
# 测试:不再要密码即成功ssh <用户名>@<服务器IP> "echo ok"原理:登录时服务器用你的公钥验证客户端持有的私钥,密码不再参与。
七、首次部署
1. 本地构建
pnpm build产物在 dist/ 目录:每个页面是 目录名/index.html,带哈希的 CSS/JS 在 _astro/ 下。可以本地先看看产物长什么样。
2. 服务器装 Nginx 并上传
# 服务器上安装 Nginx 并建站点目录ssh <用户名>@<服务器IP> "apt update && apt install -y nginx && mkdir -p /var/www/blog"
# 本地打包上传(比逐个文件 scp 快得多)tar -czf dist.tar.gz distscp dist.tar.gz <用户名>@<服务器IP>:/tmp/
# 服务器上解压ssh <用户名>@<服务器IP> "tar -xzf /tmp/dist.tar.gz -C /var/www/blog --strip-components=1"3. Nginx 站点配置
在服务器上创建 /etc/nginx/sites-available/blog:
server { listen 80 default_server; server_name _; root /var/www/blog; index index.html; charset utf-8; error_page 404 /404.html; # Astro 会构建出 404.html
gzip on; # 文本资源压缩,HTML/CSS/JS 体积减 70% 左右 gzip_types text/css application/javascript application/json image/svg+xml;
# 带哈希的静态资源:内容变了文件名就变,可以放心长缓存 location ^~ /_astro/ { expires 30d; add_header Cache-Control "public, immutable"; }}启用并重载:
ln -sf /etc/nginx/sites-available/blog /etc/nginx/sites-enabled/blogrm -f /etc/nginx/sites-enabled/default # 移除默认站点,避免抢占 80 端口nginx -t # 语法检查,必须显示 oksystemctl reload nginx浏览器打开 http://<服务器IP>,应该就能看到博客了。
Nginx 的工作方式:收到请求后按 root 找文件——/posts/xxx/ 对应 /var/www/blog/posts/xxx/index.html;请求目录但没带斜杠时自动 301 补斜杠;找不到文件就走 error_page 的 404 页。
八、全自动发布:原理详解
这是整套系统最核心的部分,值得讲透。
1. Git 基础:一次 push 背后发生了什么
先建立心智模型。Git 把文件的管理划分成几个「区域」,一次 push 的旅程是这样的:
工作区 本地仓库 远程仓库(你编辑的文件) git add→ 暂存区 git commit→ (历史记录) git push→ GitHub src/content/ (准备打包) (.git 目录)- 工作区:就是硬盘上看得见的文件,你用编辑器改 Markdown 就是在改工作区;
- 暂存区(stage):下次要打包进提交的文件清单。改动必须先经过它;
- 本地仓库:
git commit会在本机生成一个不可变的快照(提交),包含改动了什么、谁改的、什么时候; - 远程仓库:GitHub 上的仓库。
git push把本地的新提交上传过去。
2. 日常发文完整命令流
以新写一篇文章为例,完整走一遍:
# ① 先看看当前状态(哪些文件改了、哪些是新增的、哪些还没跟踪)git status
# ② 把改动加入暂存区git add src/content/posts/my-new-post.md # 精确添加单个文件# 或 git add -A # 添加全部改动(含删除、改名)
# ③ 再看一眼:改动已进入暂存区(绿色),确认没有误加的东西git status
# ④ 提交:把暂存区内容固化为一个本地快照,-m 后面是提交说明git commit -m "post: 新文章标题"
# ⑤ 推送到 GitHubgit push origin master逐个解释 git push origin master 里的两个词:
origin是远程仓库的别名,git clone时自动指向你克隆来的那个 GitHub 地址(用git remote -v查看);master是分支名,可以理解成仓库的主时间线。这条命令的意思是:把本地 master 分支上的新提交,同步到 GitHub 的 master 分支。
第一次 push 时 Git 会要求登录 GitHub。HTTPS 方式下 Windows 的凭证管理器会弹出授权窗口,登录一次之后凭证会被记住,以后不用再输。
3. 提交前后常用的辅助命令
git diff # 看工作区改了什么(还没 add 的)git diff --staged # 看暂存区里有什么(已 add 未 commit 的)git log --oneline -5 # 最近 5 条提交历史,一行一条git show # 看最新一次提交的完整改动内容提交说明(-m 里的内容)建议写清楚这次改了什么,比如 post: 新文章标题、fix: 修复导航样式。以后翻历史时,这些说明就是你的操作日志。
4. 出错了怎么办
# 情况一:忘了 add 就 commit,或者提交说明写错了git commit --amend -m "正确的说明" # 修补最近一次提交(仅限还没 push)
# 情况二:改动不想要了,想恢复到上次提交的样子git checkout -- 文件名 # 丢弃某文件未提交的改动(慎用)
# 情况三:push 被拒绝(rejected),通常是 GitHub 上有你本地没有的提交git pull --rebase origin master # 先把远端的新提交拉下来合并,再重新 push
# 情况四:已经 push 的提交想撤销git revert <提交ID> # 生成一个"反向提交"抵消它,安全且可追溯原则:只用安全的撤销手段(
revert、新建提交覆盖)。git push --force之类会重写历史的强操作,个人博客偶尔可救急,但一定要清楚自己在干什么。
到这里,代码已经在 GitHub 上了。但 git push 本身只负责把代码传到仓库,它不知道任何部署的事。真正触发自动部署的,是下面这个机制。
5. GitHub Actions:构建并发布到 dist 分支
仓库里有三个 workflow 文件,各司其职:
.github/workflows/deploy.yml— 构建站点并把产物发布到仓库的dist分支(核心);.github/workflows/build.yml— 质量检查:Node 22/23 双版本跑astro check,防手残;.github/workflows/biome.yml— 代码风格检查(biome)。
deploy.yml 的开头声明了触发条件:
on: push: branches: [ master ] # master 分支有新 push 就触发git push 后 GitHub 收到新提交,匹配到 workflow 就起一个全新的临时虚拟机(runner)按 yaml 逐步执行:安装依赖 → pnpm build → 把 dist/ 目录整体提交到仓库的 dist 分支。
为什么要经过
dist分支中转?服务器只有 2G 内存,每次都在服务器上构建又慢又吃力;而且如果让 Actions 直接 SSH 登录服务器部署,每次构建机的境外 IP 登录都会触发阿里云的「异地登录」告警。改成「GitHub 构建 → dist 分支 → 服务器拉取」,构建交给强劲的 CI 机器,服务器只做拉取和同步,全程没有任何境外登录。附带的好处:
dist分支的提交历史就是部署历史,哪次构建改了什么一目了然;出问题把服务器回退到旧的 dist 提交即可回滚。
6. 服务器端拉取部署
deploy.yml 里「发布 dist 分支」这一步的写法:
- name: Publish dist/ to dist branch run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git fetch origin dist --depth 1 || echo "首次构建:dist 分支还不存在" if git rev-parse -q --verify origin/dist >/dev/null; then git worktree add /tmp/publish origin/dist else git worktree add --orphan -b dist /tmp/publish fi rsync -a --delete --exclude .git dist/ /tmp/publish/ cd /tmp/publish git add -A if git diff --cached --quiet; then echo "构建产物无变化,跳过推送" else git commit -m "build: ${GITHUB_SHA::7}" git push origin HEAD:dist fi原理:git worktree 在不碰主工作区的前提下开一个临时工作目录指向 dist 分支,用本次构建产物整体替换后提交推送。推送用的 GITHUB_TOKEN 由 GitHub 自动注入,只对本仓库有效且永不过期,不需要在 Secrets 里配置任何密钥。
服务器这边,核心是拉取脚本 scripts/deploy.sh(已放在仓库里):发现 dist 分支有新提交,就同步到 Nginx 站点目录——注意它只做拉取和复制,不构建,几秒内完成:
#!/usr/bin/env bashset -euo pipefail
SITE_DIR="/opt/site" # dist 分支的克隆(构建产物)DEPLOY_DIR="/var/www/blog" # Nginx 站点目录BRANCH="dist"
log() { echo "[$(date '+%F %T')] $*"; }
# 文件锁:上一轮还没跑完就跳过本轮exec 9>/tmp/blog-deploy.lockflock -n 9 || exit 0
if [ ! -d "$SITE_DIR/.git" ]; then log "站点产物仓库不存在,请先执行:git clone -b dist --single-branch git@github.com:Carlinhosjoga/blog.git $SITE_DIR" exit 1fi
cd "$SITE_DIR"git fetch origin "$BRANCH"LOCAL=$(git rev-parse HEAD)REMOTE=$(git rev-parse "origin/$BRANCH")
if [ "$LOCAL" = "$REMOTE" ]; then log "没有新构建产物,跳过" exit 0fi
log "发现新构建 $LOCAL -> $REMOTE,同步上线"git reset --hard "origin/$BRANCH"rsync -az --delete --exclude .git "$SITE_DIR"/ "$DEPLOY_DIR/"log "部署完成"安全性三件事:
- 只读拉取:服务器用一把「只读 Deploy Key」访问私有仓库,即使密钥泄露,别人也只能读,推不上去、删不掉仓库;
- 构建在 GitHub 完成:服务器上不需要 Node 和 pnpm,也不怕构建失败——dist 分支没更新,服务器什么都不做,线上永远是最近一次成功构建的版本;
- rsync —delete:站点目录与构建产物严格一致,不会残留已删除页面的旧文件。
服务器初始化手把手(只需做一次,因为不构建,比在服务器上装 Node 的方案轻得多):
# ① 装 git 和 rsync(Ubuntu 一般已自带)apt install -y git rsync
# ② 生成只读拉取密钥ssh-keygen -t ed25519 -f ~/.ssh/blog_pull_key -N "" -C "blog-server-pull"cat >> ~/.ssh/config <<'EOF'Host github.com IdentityFile ~/.ssh/blog_pull_key IdentitiesOnly yesEOFcat ~/.ssh/blog_pull_key.pub# 把输出的公钥添加到 GitHub 仓库:Settings → Deploy keys → Add deploy key# (不要勾选 Allow write access)
# ③ 克隆两个仓库:/opt/blog 放源码和脚本,/opt/site 放构建产物git clone git@github.com:Carlinhosjoga/blog.git /opt/bloggit clone -b dist --single-branch git@github.com:Carlinhosjoga/blog.git /opt/site
# ④ 手动跑一次,确认能同步上线bash /opt/blog/scripts/deploy.sh
# ⑤ 加入定时任务:每 5 分钟兜底检查一次(crontab -l 2>/dev/null; \ echo "*/5 * * * * bash /opt/blog/scripts/deploy.sh >> /var/log/blog-deploy.log 2>&1") | crontab -坑点:如果
git clone或fetch报Permission denied (publickey),检查三处——公钥是否已添加为 Deploy key、~/.ssh/config是否写对了 IdentityFile、blog_pull_key权限是否是 600。另外国内服务器访问 GitHub 偶尔网络抖动,多试两次或换个时间段即可。
7. Webhook:让部署从「分钟级」变「秒级」
cron 兜底意味着最多等 5 分钟才上线。想秒级触发,给服务器加一个 webhook 接收器:GitHub 在 dist 分支更新时主动 POST 一个 HTTP 请求给服务器——这只是访问一个网址,不是登录,不会触发任何异地登录告警。服务器收到请求立刻执行部署脚本。
接收器是几十行 Python(放在服务器 /opt/blog-webhook/hook.py),只监听本机回环地址,由 Nginx 反代对外:
#!/usr/bin/env python3import hmac, hashlib, json, subprocessfrom http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
TOKEN = "<URL令牌>" # 与 Nginx 反代路径一致SECRET = "<webhook密钥>" # 与 GitHub webhook 的 Secret 一致DEPLOY = "/opt/blog/scripts/deploy.sh"LOG = "/var/log/blog-deploy.log"
class Handler(BaseHTTPRequestHandler): def do_POST(self): path = self.path if path.startswith("/webhook/"): path = path[len("/webhook"):] # Nginx 反代会带上 /webhook 前缀 if path != "/" + TOKEN: self.send_response(404); self.end_headers(); return body = self.rfile.read(int(self.headers.get("Content-Length", 0))) expect = "sha256=" + hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest() if not hmac.compare_digest(self.headers.get("X-Hub-Signature-256", ""), expect): self.send_response(403); self.end_headers(); return event = self.headers.get("X-GitHub-Event", "") if event == "ping": self.send_response(200); self.end_headers(); return if event == "push" and json.loads(body).get("ref") == "refs/heads/dist": with open(LOG, "ab") as lf: subprocess.Popen(["bash", DEPLOY], stdout=lf, stderr=lf, start_new_session=True) self.send_response(204); self.end_headers()
def log_message(self, *args): pass # 静默访问日志
ThreadingHTTPServer(("127.0.0.1", 8787), Handler).serve_forever()安全校验两层:URL 路径里有随机令牌(外人猜不到入口),请求体用 GitHub webhook Secret 做 HMAC-SHA256 签名验证(伪造请求直接 403)。而且只有 dist 分支的 push 事件才触发部署——你平时推 master 触发的 webhook 会被它自动忽略。
用 systemd 常驻(/etc/systemd/system/blog-webhook.service):
[Unit]Description=Blog deploy webhook receiverAfter=network.target
[Service]ExecStart=/usr/bin/python3 /opt/blog-webhook/hook.pyRestart=alwaysRestartSec=3
[Install]WantedBy=multi-user.target启用:systemctl daemon-reload && systemctl enable --now blog-webhook。
Nginx 加一段反代(/etc/nginx/sites-available/blog):
# GitHub webhook 部署通知(路径含随机令牌,仅 dist 分支事件触发部署) location = /webhook/<URL令牌> { proxy_pass http://127.0.0.1:8787; add_header X-Robots-Tag "noindex, nofollow" always; }nginx -t && systemctl reload nginx 生效。最后到 GitHub 仓库:Settings → Webhooks → Add webhook,Payload URL 填 http://<服务器IP>/webhook/<URL令牌>,Content type 选 application/json,Secret 填与脚本一致的密钥,触发事件保持默认(Just the push event)。
之后每次发布的时序:git push → GitHub 构建(约 2 分钟)→ dist 分支更新 → webhook 秒达 → 服务器同步上线。全程无登录、无手工操作。
8. 完整数据流
本地写文章 (src/content/posts/xxx.md) │ │ git add / commit / push (master) ▼GitHub 仓库 ←── build.yml / biome.yml 质量检查 │ │ deploy.yml:pnpm build → 产物提交到 dist 分支 ▼dist 分支更新 ──webhook 秒级通知──▶ 服务器执行 deploy.sh │ │ │ │ cron 每 5 分钟兜底 ▼ ▼ └──────▶ git 拉取 dist → rsync → /var/www/blog → Nginx → 访客构建永远在 GitHub 完成:服务器只做几秒钟的拉取同步,2G 内存毫无压力;产物和源码永远一一对应,出问题回滚就是把服务器 reset 到旧的 dist 提交。
九、日常更新流程
配置完之后,写文章只剩三步:
# 1. 写文章:在 src/content/posts/ 新建 .md 文件# 2. 提交:git add -A && git commit -m "post: 文章标题"# 3. 推送:git push约两三分钟内自动上线:GitHub 构建(约 2 分钟)→ dist 分支更新 → webhook 秒级触发服务器同步。看进度两个渠道:GitHub 仓库 Actions 标签页看构建日志(绿勾 = 构建成功);服务器上 tail -f /var/log/blog-deploy.log 看部署日志。
十、后续计划
- 域名备案通过后:DNS 解析到服务器,Nginx 配置
server_name,用 certbot 免费签发 HTTPS 证书(自动续期); - 追番页数据是构建时拉取的,计划加一个每日定时构建(即使没有新文章也刷新一次),追番进度自动更新;
- 流量起来后在服务器前套一层国内 CDN,源站带宽压力趋近于零。
整个搭下来最大的感受:静态博客的复杂度不在写页面,而在把「写完之后的事」自动化。一旦发布管道跑通,写文章这件事就变得毫无心理负担——这也正是坚持输出的前提。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!












