从零搭建个人博客:手把手完整教程与踩坑记录

5692 字
28 分钟
从零搭建个人博客:手把手完整教程与踩坑记录
AI 智能总结内容由 AI 生成,仅供参考

从零构建个人博客的完整流程被一步步拆解:配合 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 安装包,一路下一步。装完重开一个终端验证:

Terminal window
node -v # 输出 v22.x.x 即可
npm -v

老版本不够就先卸载再装新版,Node 多版本共存工具(nvm-windows)是进阶玩法,入门没必要。

2. pnpm(≥ 11)#

Node 自带 npm,用它装 pnpm:

Terminal window
npm install -g pnpm
pnpm -v

坑点:如果项目用 .npmrc 锁定了 pnpm 版本,直接运行 pnpm 时它会自动切换到项目要求的版本,不用手动降级。

3. Git#

去 git-scm.com 下载安装,安装选项保持默认即可。第一次用 Git 需要配置身份(每条提交记录的署名):

Terminal window
git config --global user.name "你的用户名"
git config --global user.email "你的邮箱"

坑点:Windows 下有些终端装完 Git 后找不到命令,重开一个终端窗口就好;还不行就把 Git 的 cmd 目录加进 PATH 环境变量。

三、初始化项目#

1. 拿到主题代码#

在 GitHub 上把 Firefly 主题仓库 Fork(或右上角 Use this template)到自己账号下,然后克隆到本地:

Terminal window
git clone https://github.com/<你的用户名>/<仓库名>.git
cd <仓库名>

建议直接在 GitHub 上把仓库设为 Private——文章草稿、服务器配置这些不需要公开。

2. 安装依赖并启动#

Terminal window
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-21
description: 摘要,会显示在列表页
tags: [标签1, 标签2]
category: 技术
draft: false
slug: my-first-post # 不填则用「文件夹+文件名」当 URL
---
正文用 Markdown 写……

保存后回浏览器看效果。完整的逐字段说明、正文语法、发布流程都在使用指南里,这里不重复。

五、踩坑记录#

搭的过程中遇到的三个印象最深的坑,写下来防止后人(和未来的我)重踩。

坑 1:不带斜杠的 URL 404#

Astro 配置了 trailingSlash: "always" 后,/posts/xxx(无斜杠)直接 404。而生产环境静态托管对无斜杠请求一般会自动 301 补斜杠,本地却不行,行为不一致。

解决:把配置改成 "ignore",再加一个中间件统一重定向:

src/middleware.ts
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 忽略列表(治本),再写个循环脚本崩溃后自动重启(兜底):

Terminal window
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 免密登录#

每次输密码太麻烦,配一下密钥登录:

Terminal window
# 本地生成密钥对(一路回车)
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. 本地构建#

Terminal window
pnpm build

产物在 dist/ 目录:每个页面是 目录名/index.html,带哈希的 CSS/JS 在 _astro/ 下。可以本地先看看产物长什么样。

2. 服务器装 Nginx 并上传#

Terminal window
# 服务器上安装 Nginx 并建站点目录
ssh <用户名>@<服务器IP> "apt update && apt install -y nginx && mkdir -p /var/www/blog"
# 本地打包上传(比逐个文件 scp 快得多)
tar -czf dist.tar.gz dist
scp 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";
}
}

启用并重载:

Terminal window
ln -sf /etc/nginx/sites-available/blog /etc/nginx/sites-enabled/blog
rm -f /etc/nginx/sites-enabled/default # 移除默认站点,避免抢占 80 端口
nginx -t # 语法检查,必须显示 ok
systemctl 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. 日常发文完整命令流#

以新写一篇文章为例,完整走一遍:

Terminal window
# ① 先看看当前状态(哪些文件改了、哪些是新增的、哪些还没跟踪)
git status
# ② 把改动加入暂存区
git add src/content/posts/my-new-post.md # 精确添加单个文件
# 或 git add -A # 添加全部改动(含删除、改名)
# ③ 再看一眼:改动已进入暂存区(绿色),确认没有误加的东西
git status
# ④ 提交:把暂存区内容固化为一个本地快照,-m 后面是提交说明
git commit -m "post: 新文章标题"
# ⑤ 推送到 GitHub
git push origin master

逐个解释 git push origin master 里的两个词:

  • origin 是远程仓库的别名,git clone 时自动指向你克隆来的那个 GitHub 地址(用 git remote -v 查看);
  • master 是分支名,可以理解成仓库的主时间线。这条命令的意思是:把本地 master 分支上的新提交,同步到 GitHub 的 master 分支。

第一次 push 时 Git 会要求登录 GitHub。HTTPS 方式下 Windows 的凭证管理器会弹出授权窗口,登录一次之后凭证会被记住,以后不用再输。

3. 提交前后常用的辅助命令#

Terminal window
git diff # 看工作区改了什么(还没 add 的)
git diff --staged # 看暂存区里有什么(已 add 未 commit 的)
git log --oneline -5 # 最近 5 条提交历史,一行一条
git show # 看最新一次提交的完整改动内容

提交说明(-m 里的内容)建议写清楚这次改了什么,比如 post: 新文章标题、fix: 修复导航样式。以后翻历史时,这些说明就是你的操作日志。

4. 出错了怎么办#

Terminal window
# 情况一:忘了 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 bash
set -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.lock
flock -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 1
fi
cd "$SITE_DIR"
git fetch origin "$BRANCH"
LOCAL=$(git rev-parse HEAD)
REMOTE=$(git rev-parse "origin/$BRANCH")
if [ "$LOCAL" = "$REMOTE" ]; then
log "没有新构建产物,跳过"
exit 0
fi
log "发现新构建 $LOCAL -> $REMOTE,同步上线"
git reset --hard "origin/$BRANCH"
rsync -az --delete --exclude .git "$SITE_DIR"/ "$DEPLOY_DIR/"
log "部署完成"

安全性三件事:

  1. 只读拉取:服务器用一把「只读 Deploy Key」访问私有仓库,即使密钥泄露,别人也只能读,推不上去、删不掉仓库;
  2. 构建在 GitHub 完成:服务器上不需要 Node 和 pnpm,也不怕构建失败——dist 分支没更新,服务器什么都不做,线上永远是最近一次成功构建的版本;
  3. rsync —delete:站点目录与构建产物严格一致,不会残留已删除页面的旧文件。

服务器初始化手把手(只需做一次,因为不构建,比在服务器上装 Node 的方案轻得多):

Terminal window
# ① 装 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 yes
EOF
cat ~/.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/blog
git 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 python3
import hmac, hashlib, json, subprocess
from 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 receiver
After=network.target
[Service]
ExecStart=/usr/bin/python3 /opt/blog-webhook/hook.py
Restart=always
RestartSec=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 提交。

九、日常更新流程#

配置完之后,写文章只剩三步:

Terminal window
# 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,源站带宽压力趋近于零。

整个搭下来最大的感受:静态博客的复杂度不在写页面,而在把「写完之后的事」自动化。一旦发布管道跑通,写文章这件事就变得毫无心理负担——这也正是坚持输出的前提。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

从零搭建个人博客:手把手完整教程与踩坑记录
https://li8.me/posts/1/
作者
八奈奈
发布于
2026-09-21
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
八奈奈
记录技术与生活
公告
欢迎来到「二次元宅男录」!这里记录技术与生活,欢迎常来逛逛。
分类
标签
站点统计
文章
10
分类
4
标签
18
总字数
17,017
运行时长
0 天
最后活动
0 天前
文章目录