ARTICLE 2026—0801—01FOLIO 2026—0801—01ACRETIONDISKARCHIVUM PERSONAE

我的博客是怎么搭起来的:技术栈总结与从零搭建教程

用 Astro 静态生成 + Markdown 写作 + 管理面板 + GitHub Actions 自动部署,配上 Waline 留言和不蒜子统计——一文讲清这套博客每一层用了什么、为什么,以及从零复刻的完整步骤。

我的博客是怎么搭起来的:技术栈总结与从零搭建教程

经常有人问我:「这个博客是怎么搭的?看起来不像普通博客模板。」这篇就把整套技术栈从头到尾讲清楚,并给出从零复刻的步骤。不追求面面俱到,只讲「我实际用了什么、为什么这么选」。

一句话总览

一个纯静态站点 + 一个本地管理面板 + 一条自动部署流水线

  • 网页本身由 Astro 在构建时生成成纯 HTML(零服务器、零数据库、零运行时依赖);
  • 文章是 Markdown 文件,用 Obsidian 或浏览器里的管理面板写作;
  • 写完 git pushGitHub 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 实时预览

管理面板是这套博客「生产力」的核心:上传图片自动插入 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

  1. 建 GitHub 仓库(用户名.github.io 或普通仓库开 Pages);
  2. 复制 .github/workflows/deploy.yml(上文那份);
  3. 仓库根目录放空文件 .nojekyll
  4. git push,一分钟后访问 https://你的用户名.github.io

5. 加管理面板(可选但强烈推荐)

装三个依赖:expressgray-mattermulter。后端一个文件提供文章 CRUD + 图片上传 + git 推送接口,前端一个 HTML 文件。这个仓库的 admin-server.mjs 可以直接抄来改。

6. 加留言(可选)

docs/WALINE_DEPLOY.md:Vercel 一键部署 Waline → 配 KV 数据库和 JWT_TOKEN → 在管理面板填入服务地址 → 前端按 Waline API 契约实现读取/发布。

日常使用:发布一篇新文章

  1. :Obsidian 打开 src/content/blog/ 写 Markdown,或用管理面板在线写;
  2. :管理面板里直接粘贴图片,自动压缩并推送到图片仓库,返回外链插入正文;
  3. git push origin main(或管理面板点「⬆ 推送」);
  4. :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 都能搭出类似的博客。但这一套组合下来,写作体验、部署成本和加载速度的平衡,是我目前最满意的。如果你也想要一个「自己的地盘」,照上面的步骤走一遍,半天就能上线。

读完了FINIS

文章到这里结束本条目至此结束

评论COMMENTS IN THE MARGIN— ITEMS
仅文字 · 最多 300 字 · 显示 IP 属地仅文字 · 不能附加图片 · 发送后标注 IP 属地0 / 300