我的博客是怎么搭起来的:技术栈总结与从零搭建教程
用 Astro 静态生成 + Markdown 写作 + 管理面板 + GitHub Actions 自动部署,配上 Waline 留言和不蒜子统计——一文讲清这套博客每一层用了什么、为什么,以及从零复刻的完整步骤。
我的博客是怎么搭起来的:技术栈总结与从零搭建教程
经常有人问我:「这个博客是怎么搭的?看起来不像普通博客模板。」这篇就把整套技术栈从头到尾讲清楚,并给出从零复刻的步骤。不追求面面俱到,只讲「我实际用了什么、为什么这么选」。
一句话总览
一个纯静态站点 + 一个本地管理面板 + 一条自动部署流水线。
- 网页本身由 Astro 在构建时生成成纯 HTML(零服务器、零数据库、零运行时依赖);
- 文章是 Markdown 文件,用 Obsidian 或浏览器里的管理面板写作;
- 写完
git push,GitHub Actions 自动构建并发布到 GitHub Pages; - 评论交给 Waline(Vercel + 云数据库),访问量用不蒜子统计。
下面是全站技术栈一览:
| 层 | 技术 | 作用 |
|---|---|---|
| 静态站点生成 | Astro 7(static 输出) | 构建时把页面渲染成纯 HTML |
| 内容管理 | Markdown + Content Collections | 文章即文件,带结构校验 |
| 写作工具 | Obsidian(Vault = 文章目录)+ 管理面板 | 本地写作 / 浏览器写作 |
| 管理面板 | Express + 原生 HTML/CSS/JS | 文章 CRUD、传图、一键推送 |
| 图片托管 | 独立图床仓库 + jsDelivr CDN | 图片独立仓库托管、WebP 外链加载,境内加速 |
| 部署 | GitHub Actions + GitHub Pages | push 即上线 |
| 评论系统 | Waline(Vercel + Neon/Upstash KV) | 留言、IP 属地、字数限制 |
| 访问统计 | 不蒜子 | 文章页阅览次数 |
| 设计 | 纯 CSS 手工定制 | 「私人档案」视觉风格 |
先看成品
这是博客首页——「私人档案 / 编目册」风格,左侧是站点标题与印章,右侧是文章列表,支持从新至旧 / 从旧至新切换:

文章目录页(「卷册目录」),每篇文章有 FOLIO 编号、日期、分类标签:

第一层:Astro 静态站点
为什么是 Astro
博客是「读多写少」的内容站,静态生成是最划算的方案:构建一次,之后全世界访问都是 CDN 上现成的 HTML,速度快、零维护、几乎不可能被攻击。
Astro 在静态站工具里属于「长得像现代前端框架、用起来像写 HTML」的一类——页面文件就是 .astro(HTML + 少量组件脚本),默认输出零 JavaScript,加载极快。
核心配置文件 astro.config.mjs 只有这么点东西:
import { defineConfig } from 'astro/config';
export default defineConfig({
site: 'https://anacretiondisk9986.github.io',
output: 'static', // 纯静态输出,不需要服务器
});
(图片的托管方案在下面「图片:拖进去就完了」小节讲。)
文章即文件:Content Collections
Astro 内置 Content Collections:把文章放在 src/content/blog/,用一个 schema 声明每篇文章必须有哪些字段,写错了构建直接报错:
// src/content.config.ts
const blog = defineCollection({
loader: glob({ base: './src/content/blog', pattern: ['**/*.{md,mdx}'] }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
于是每篇文章就是一个带 frontmatter 的 Markdown 文件:
---
title: 文章标题
description: 一句话摘要
pubDate: "2026-08-01"
tags: [技术, 教程]
draft: false
---
正文用 Markdown 写……
页面(首页、目录、详情)在构建时读取这些文件生成 HTML。想看文章长什么样,这是详情页——左侧正文,右侧是「档案登记条」侧栏(日期、编号、标签、阅览次数、导出 PDF):

第二层:写文章的两条路
路一:Obsidian 本地写作
src/content/blog/ 整个目录就是一个 Obsidian Vault:用 Obsidian 打开它,就能用本地编辑器写作,支持双链、模板、本地图片。目录里内置了 _templates/blog-post.md 模板,新建文章自动带好 frontmatter。
路二:浏览器里的管理面板
不想开本地编辑器时,有配套的管理面板:npm run admin 后访问 http://localhost:4322/admin(也可以直接双击仓库里的 启动管理面板.bat)。
它是零前端框架的:后端一个 Express 文件(admin-server.mjs),前端一个纯 HTML 文件,功能却不少——文章增删改查、图片拖放/粘贴上传、Markdown 实时预览、一键推送、画廊与留言管理:

管理面板是这套博客「生产力」的核心:上传图片自动插入 Markdown,Ctrl+S 保存,点一下「⬆ 推送」就直接发布。
图片:拖进去就完了
图片托管在独立的公开图片仓库(AnAcretiondisk9986/blog-images),通过 jsDelivr CDN 外链加载:
- 管理面板上传/粘贴图片 → 后端自动转 WebP(quality 78、超 1920px 等比缩小,一张 2MB 截图压完不到 100KB)→ git 推送到图片仓库 → 返回
https://cdn.jsdelivr.net/gh/AnAcretiondisk9986/blog-images@main/image/xxx.webp外链,直接插入 Markdown; - jsDelivr 有国内节点,境内访问比 github.io 快得多,且图片不占博客仓库体积和 Pages 带宽;
- 画廊独立收藏另有原图归档(
image/original/),查看器里可一键切换原图/压缩版。
早期方案是把图片留在博客仓库、构建时用 sharp 压缩(详见《博客图片压缩优化技术报告》)。但 41MB 原图入库让仓库膨胀、境内访问 github.io 又慢,最终改为独立图床 + CDN 外链:博客仓库瘦身到纯代码,写作时粘贴即用、零额外步骤。
第三层:自动部署
部署走 GitHub Actions + GitHub Pages,流水线写在 .github/workflows/deploy.yml 里:
on:
push:
branches: [main] # 一推 main 就触发
jobs:
build:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: npm }
- run: npm ci
- run: npm run build # 构建出 dist/
- uses: actions/upload-pages-artifact@v3
with: { path: ./dist }
deploy:
needs: build
uses: actions/deploy-pages@v4 # 发布到 GitHub Pages
也就是说,写文章 → git push → 等一两分钟 → 上线,全程没有手动步骤。
💡 两个关键细节:
- 仓库根目录放了一个空文件
.nojekyll——否则 GitHub Pages 会用 Jekyll 解析,把.astro文件当模板报错;- 本地
.githooks/pre-commit会在提交前自动跑一遍构建,构建失败就不让你提交,避免把坏代码推上线。
第四层:留言与统计
Waline 评论(免费方案)
留言页长这样——仿 B 站评论区版式,有楼层号、IP 属地、300 字限制:

后端用 Waline(一个开源评论服务):部署到 Vercel(免费),数据库用 Vercel 自带的 KV(Upstash Redis)或 Neon PostgreSQL。它自带 ip2region 离线库,服务端直接解析 IP 属地,不需要任何第三方地理 API。部署步骤在仓库的 docs/WALINE_DEPLOY.md 里写得很全,大概十分钟能搞定。
不蒜子统计
文章页侧栏的「阅览次数」用的是不蒜子——一个国内可直连的免费计数服务,按页面 URL 统计,一行脚本搞定。
第五层:视觉风格
全局样式在 src/styles/global.css(单文件 58KB,纯手写 CSS),设计语言是「私人档案 / 编目册」:米白纸面 + 纸张颗粒纹理、印章、FOLIO 编号、标本图版式画廊。这套视觉是博客最有辨识度的地方:


从零复刻:照着做一遍
如果你想搭一套类似的博客,按下面顺序做,每一步都是独立的:
1. 初始化 Astro
npm create astro@latest my-blog
cd my-blog
npm install
npm run dev # 本地预览 http://localhost:4321
选 Empty 模板,TypeScript: yes(schema 校验需要)。
2. 建 Content Collections
建 src/content.config.ts(照抄上面的 schema),再建 src/content/blog/ 放文章。写一篇 hello.md 试试。
3. 写三个关键页面
src/pages/index.astro:首页,列出全部文章;src/pages/blog/[...id].astro:文章详情页([...id]是 Astro 的动态路由,按 slug 取文章);src/pages/blog/index.astro:文章目录。
读取文章的核心代码就几行:
---
import { getCollection } from 'astro:content';
const posts = (await getCollection('blog'))
.filter(p => !p.data.draft)
.sort((a, b) => b.data.pubDate - a.data.pubDate);
---
{posts.map(post => <a href={`/blog/${post.id}/`}>{post.data.title}</a>)}
样式可以在 src/styles/global.css 里慢慢打磨——这是最花时间也最出效果的一步。
4. 部署到 GitHub Pages
- 建 GitHub 仓库(用户名.github.io 或普通仓库开 Pages);
- 复制
.github/workflows/deploy.yml(上文那份); - 仓库根目录放空文件
.nojekyll; git push,一分钟后访问https://你的用户名.github.io。
5. 加管理面板(可选但强烈推荐)
装三个依赖:express、gray-matter、multer。后端一个文件提供文章 CRUD + 图片上传 + git 推送接口,前端一个 HTML 文件。这个仓库的 admin-server.mjs 可以直接抄来改。
6. 加留言(可选)
按 docs/WALINE_DEPLOY.md:Vercel 一键部署 Waline → 配 KV 数据库和 JWT_TOKEN → 在管理面板填入服务地址 → 前端按 Waline API 契约实现读取/发布。
日常使用:发布一篇新文章
- 写:Obsidian 打开
src/content/blog/写 Markdown,或用管理面板在线写; - 图:管理面板里直接粘贴图片,自动压缩并推送到图片仓库,返回外链插入正文;
- 发:
git push origin main(或管理面板点「⬆ 推送」); - 等:Actions 自动构建部署,一分钟内线上可见。
常用命令汇总:
| 命令 | 作用 |
|---|---|
npm run dev |
本地开发服务器 |
npm run build |
生产构建(产物在 dist/) |
npm run preview |
本地预览构建结果 |
npm run admin |
启动管理面板 |
npm run mock:waline |
本地留言 mock 服务 |
git push origin main |
发布(自动触发部署) |
结语
整套博客的哲学可以概括为三点:内容即文件(Markdown 可迁移、可版本管理)、构建期干完所有重活(压缩、渲染、生成都发生在 build 时,线上只有静态文件)、少依赖(管理面板零前端框架,能手写就手写)。
技术栈里没有一样是「必须」的——换 VitePress、Hugo、Hexo 都能搭出类似的博客。但这一套组合下来,写作体验、部署成本和加载速度的平衡,是我目前最满意的。如果你也想要一个「自己的地盘」,照上面的步骤走一遍,半天就能上线。