静态站点生成器(SSG)的核心职责是将 Markdown 文稿与页面模板编译为标准 HTML、CSS 和 JSON 索引。相比在浏览器端动态请求接口渲染,预编译生成的静态页面首屏体积更小,也便于搜索引擎和 RSS 阅读器直接抓取。
本文记录基于 Astro 的静态博客工程目录划分、Markdown 渲染管线、atom.xml 与 search.json 生成逻辑,以及构建阶段的静态检查脚本。
工程目录划分与文件职责
初始化博客仓库(npm create astro@latest)时,建议将文章原稿、静态资源、页面组件和检查脚本按职责分开存放:
| 目录路径 | 职责说明 | 是否纳入 Git | 是否进入生产 public/ |
|---|---|---|---|
source/_posts/ |
存放 Markdown 技术文章(.md),包含 YAML Front Matter |
是 | 仅 draft: false 且 published: true 的文章编译为 HTML 后进入 |
source/_data/ |
存放结构化数据文件(如 projects.yml 开源项目清单、资源许可记录) |
是 | 构建期由 TypeScript 读取渲染,原始 YAML 文件不直接暴露 |
static/ |
存放无需编译的静态资源(images/、fonts/、robots.txt、favicon.svg) |
是 | 构建时原样复制到 public/ 对应路径 |
src/ |
Astro 页面路由(pages/)、全局布局(layouts/)、组件(components/)与样式(styles/) |
是 | 编译为静态 .html 与 .css |
tools/ 与 docs/ |
敏感信息扫描、标签词表、死链检查与插图完整性校验脚本 | 是 | 不进入公开 public/ 目录 |
public/ |
最终静态构建产物目录(HTML/CSS/JS/SVG/WebP/XML/JSON) | 否(加入 .gitignore) |
同步至生产服务器 /srv/blog/releases/<版本号>/ |
核心构建配置:固定链接与目录索引输出
为了让编译生成的静态文件结构能够直接配合 Nginx 的目录索引分发,在项目根目录配置 astro.config.mjs:
1 | // astro.config.mjs — 静态博客构建配置 |
其中三个配置项直接影响线上路由行为:
output: 'static'与build.format: 'directory': 动态路由src/pages/posts/[slug].astro会把source/_posts/linux-systemd.md编译为public/posts/linux-systemd/index.html。访问/posts/linux-systemd/时,Nginx 直接按目录索引返回index.html,既保持了无.html后缀的干净 URL,又不需要编写额外的重写规则。publicDir: './static'与outDir: './public': 每次构建前先执行清理脚本(清空旧的public/与.astro/缓存),避免已删除或转为草稿的文章残留在输出目录中。devToolbar: { enabled: false }: 关闭开发模式下的浮动调试工具条,使本地预览画面与线上构建结果保持一致。
构建期内容解析、草稿隔离与静态索引生成
在 src/lib/content.ts 中编写统一的内容加载与渲染模块,在构建阶段完成以下处理:
1. 草稿过滤(Draft Isolation)
读取 source/_posts/*.md 时,仅允许 draft: false 且 published: true 的文章进入公开集合。未完成的草稿(draft: true)在构建期直接过滤,不会生成详情页 HTML,也不会出现在首页、归档、RSS 或搜索索引中。
2. 提取章节目录(TOC)与阅读时长估算
使用 Marked 自定义渲染器在编译期遍历 Markdown 语法树:
- 为所有
<h2>和<h3>标题生成锚点id,并收集为headings数组供文章右侧目录组件渲染; - 为代码块生成带行号列(
td.gutter)与代码列(td.code)的双列结构,并附上data-lang语言标识; - 根据中文字符数(按
300 字/分钟)与英文词数(按200 词/分钟)在构建期估算每篇文章的阅读时长与总字数。
3. 生成 RSS、Sitemap 与本地搜索索引
在 src/pages/ 下通过三个静态端点脚本在构建期生成机器可读索引:
src/pages/atom.xml.ts→public/atom.xml:生成符合 RFC 4287 标准的 Atom 1.0 订阅源,包含最新 20 篇公开文章的标题、摘要、永久链接与 RFC 3339 时间戳;src/pages/sitemap.xml.ts→public/sitemap.xml:枚举全站公开且非noindex的规范 URL,供搜索引擎抓取;src/pages/search.json.ts→public/search.json:将公开文章的标题、分类、标签、摘要与纯文本正文导出为 JSON 索引,供前端本地全文检索按需加载。
本地开发、构建校验与远程版本同步
日常写作与发布主要使用以下三条命令:
1 | # 1. 按 package-lock.json 安装依赖 |
当 npm run check 通过后,使用 rsync 将本地生成的 public/ 增量同步到服务器 /srv/blog/releases/ 下带时间戳的新版本目录中:
1 | # 在本地或 CI 发布端执行:创建新版本目录并增量同步静态产物 |
将产物同步到独立的 releases/${RELEASE_ID}/ 目录而非直接覆盖线上目录,主要有两点好处:
- 避免传输过程中的文件不一致:直接覆盖线上目录时,若在
rsync传输中途有请求进来,可能会同时读到新版 HTML 与尚未传完的旧版 CSS; - 便于快速回滚:上线后若发现排版或配置问题,将
/srv/blog/releases/current软链接指回上一个时间戳目录即可恢复。
常见构建问题排查
| 故障现象 | 根因定位 | 处理方法 |
|---|---|---|
| 文章内引用的 SVG 或 WebP 图片显示 404 | 静态图片未放入 static/images/ 对应路径,或文件名大小写不一致 |
Linux 文件系统区分大小写,统一使用小写加连字符命名并运行 npm run check |
将某篇文章改为 draft: true 后,public/posts/ 下仍存在对应 HTML |
构建前未清空上一次生成的 public/ 目录 |
在 build 脚本前串联 clean 步骤清空 public/ 与 .astro/ |
RSS 阅读器报错 Invalid RFC3339 timestamp |
Front Matter 中的 date 或 updated 格式不规范 |
统一采用 YYYY-MM-DD HH:mm:ss 格式,由构建管线转换为 ISO 8601 / RFC 3339 |