写博客两年,攒了 80 多篇文章,版本从 v0.1 迭代到 v1.8。每次改完配置或加了新功能都会更新 changelog,但从来没有一篇文章把整个架构串起来。
这篇文章来补这个缺口。
整体架构
本地 Markdown 文件
↓
Astro Content Collections(Zod schema 校验)
↓
构建管道(remark 插件 + rehype 插件 + MDX)
↓
静态 HTML + JSON 数据文件
↓
Cloudflare Pages(全球 CDN 部署)
↓
GitHub Actions 定时触发重建(自动更新已到期文章)整条链路没有数据库、没有服务端渲染逻辑、没有动态后端。内容是纯 Markdown,部署是纯静态,发布靠 Git push。
内容层:Content Collections + Zod
博客文章全部是 Markdown 文件,放在 src/content/blog/ 目录下。每篇文章的 frontmatter 由 Zod schema 定义:
// src/content/config.ts
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
category: z.string().optional(),
tags: z.array(z.string()).optional(),
series: z.string().optional(),
seriesOrder: z.number().optional(),
// 生长状态:seed=种子 / bud=发芽 / bloom=绽放
growth: z.enum(['seed', 'bud', 'bloom']).optional(),
// 手动指定别名,避免 URL 冲突
slug: z.string().optional(),
}),
});这个 schema 有什么实际作用?Astro 在构建时会校验所有文章的 frontmatter。字段类型错了、日期格式不对,build 直接失败并告诉你哪篇文章有问题。
这比 WordPress 那种”发布时没问题,访问时报错”的做法好多了。
构建管道:四层插件链
Astro 默认把 Markdown 转成 HTML,但我的博客需要更多:脚注、维基链接语法、代码高亮、图片懒加载、结构化数据。
这些全部通过 remark 和 rehype 插件实现:
// astro.config.mjs
import remarkFootnote from 'remark-footnotes';
import remarkDirective from 'remark-directive';
import remarkEmoji from 'remark-emoji';
import { remarkHighlight } from './src/plugins/remark-highlight.js';
import { remarkWikiLinks } from './src/plugins/remark-wiki-links.js';remark-footnotes:原生脚注语法[^1]自动变成脚注区块remark-directive:支持::tip、:::warning等自定义指令块remark-wiki-links:[[维基链接]]语法,生成内部链接remark-highlight:代码块语法高亮
这套插件链的好处是:内容作者写文章时用 Markdown 惯用语法,不需要记任何自定义标签,但渲染出来的页面有丰富功能。
日期过滤:已发布文章的自动化管理
写技术博客有个痛点:预先写好一批文章,按节奏排期发布,但 build 时这些”未来文章”也会被构建进索引和 sitemap。
v1.7.2 做了 filterPublished 解决这个问题:
// src/utils.ts
export function filterPublished<T extends { data: { pubDate: Date } }>(
posts: T[]
): T[] {
const now = Date.now();
return posts.filter(post => post.data.pubDate.getTime() <= now);
}这个函数接入了博客的 13 个读取文章的位置:
- 首页文章列表
- 归档页(所有已发布文章)
- 分类页(分类索引 + 分类详情)
- 标签页(标签索引 + 标签详情)
- 系列页(系列索引 + 系列详情)
- 相关文章算法
- 上下篇文章导航
- RSS 全文输出
- 搜索索引(Pagefind)
构建结果验证:84 篇文章中,80 篇已发布(pubDate ≤ 今天),4 篇未来文章(8/10 到 9/4)完全不生成页面、不进索引、不进 sitemap。
部署链路:GitHub Actions + Cloudflare Pages
作者本地:git push
↓
GitHub 仓库:触发 Cloudflare Pages 自动部署
↓
Cloudflare Pages:构建 Astro 项目,输出静态文件
↓
全球 CDN 边缘节点分发但光靠这个还不够。文章按排期发布,到期了谁去触发重建?答案是 GitHub Actions 定时任务。
每天北京时间 00,Actions 执行 scan-posts.sh 脚本:
- 扫描
src/content/blog/下所有文章的 frontmatter 日期 - 找到当天到期(pubDate === 今天)的文章
- 生成构建日志写入
src/data/build-log.md - commit + push 到 GitHub,触发 Cloudflare Pages 重建
这意味着:文章写好、设好 pubDate,到期那天自动上线,不需要手动操作。
搜索:Pagefind 的客户端全文搜索
博客没有服务器端搜索,用的是 Pagefind——一个在构建时生成搜索索引、运行时在浏览器里做全文搜索的工具。
# 安装
npx astro-add pagefind
# 使用(页面组件)
<PagefindUI />构建时 Pagefind 扫描所有 HTML 文件,建立倒排索引,打包成 ~50KB 的 wasm 文件。读者搜索时,Pagefind 在浏览器里直接查索引,不需要请求服务器。
优势:零服务器成本、搜索速度极快、不需要任何后端 API。劣势:搜索索引在构建时生成,对已发布文章实时性无影响,但如果文章内容改了需要等下一次构建完成才能搜到新内容。
数据文件层:JSON 配置与 changelog
博客不只有文章。还有大量站点元数据:
src/data/
changelog.json ← 版本历史,每次发版更新
build-log.md ← 构建日志,Actions 自动生成
books_music_movies.json ← 书影音记录
steam_games.json ← 游戏记录
github_projects.json ← GitHub 项目列表
gear.json ← 装备清单
daily-quotes.json ← 每日一言这些 JSON 文件在构建时被 getCollection 或 import 读入页面组件,渲染成页面内容。比如关于页的”游戏时长”模块,就是读取 steam_games.json 动态渲染的。
好处是数据与代码分离:要更新游戏时长,不需要改页面组件,改 JSON 就行。
样式架构:CSS 变量 + 作用域样式
博客用了 Astro 的 scoped CSS 特性,每个组件的样式只作用于该组件,不会泄露到全局。
全局 CSS 变量定义在 src/styles/global.css 里:
:root {
--text-primary: #1a1a1a;
--text-secondary: #4a4a4a;
--bg-primary: #ffffff;
--accent: #3b82f6;
/* 更多变量... */
}每个页面组件引用全局变量,组件自己的样式放在 .astro 文件里:
<style>
.post-title {
color: var(--text-primary);
font-size: 1.5rem;
}
</style>这样改主题色只需要改 global.css 的变量值,全站生效,不需要逐个组件修改。
三层配置架构
博客配置分三层,每层职责不同:
| 层级 | 文件 | 职责 | 修改频率 |
|---|---|---|---|
| 框架层 | astro.config.mjs | 集成插件、适配器、构建选项 | 极低 |
| 站点层 | src/config/site.config.mjs | 站名、导航、社交链接、园丁守则 | 低 |
| 内容层 | src/content/config.ts | frontmatter schema、文章字段定义 | 极低 |
这个分层解决了”改一个导航链接要翻几百行配置文件”的问题。日常操作基本只动站点层,其他两层设好基本不动。
回头看这两年
v0.1 的时候,所有配置都在 astro.config.mjs 里,60 行。
现在:
astro.config.mjs:框架配置 + CSP 安全策略 + 插件链site.config.mjs:站点元数据 + 导航 + 社交src/content/config.ts:文章 schemasrc/data/*.json:各种数据文件src/plugins/:自定义 remark/rehype 插件scripts/:自动化脚本(OG 图生成、别名映射、访问数据同步等)
架构没有”正确答案”。随着需求增长,某些当时合理的做法会变得笨拙,改就是了。重要的是每次迭代之后,博客能继续正常跑,读者感觉不到变化,开发者(也就是我)改起来更顺手。
两年迭代下来的最大感受:好的架构不是一开始设计出来的,是被需求一点点逼出来的。