用 Obsidian 写博客:我的发布流水线
让写作回到 Obsidian,让发布交给一条命令。这套流水线解决了我三年来「笔记写得很爽、发博客很痛苦」的割裂感。
我的知识库在 Obsidian 里,我的博客是静态站点。中间隔着一道鸿沟:手动复制粘贴。
复制的时候要处理图片路径、要改 frontmatter、要删掉内部链接。写一篇文章的兴奋感,往往就在这些琐事里消耗干净了。
所以我把这条流水线自动化了。核心思路只有一句话:
Obsidian 负责写作,博客仓库负责发布,脚本负责搬运。
目录结构
博客站点的内容目录就是唯一事实来源:
blog/
├─ src/
│ ├─ content/
│ │ └─ posts/ ← 文章都在这里(.md)
│ ├─ components/
│ └─ pages/
├─ public/ ← 图片、favicon
└─ scripts/
└─ deploy.sh
Obsidian 直接把这个 posts/ 目录当成一个 vault(或者用软链接挂进主库),这样写的时候就有双链、图谱、模板——所有 Obsidian 的能力。
Frontmatter 模板
我给自己定了一个最小字段集:
---
title: 文章标题
description: 一句话摘要,会用在列表页和 SEO
pubDate: 2026-08-05
updatedDate: 2026-08-06 # 可选
tags: ['写作', '工具']
featured: false # 是否上首页精选
draft: true # 草稿:不会出现在构建产物里
---
配合 Obsidian 的模板插件,新建文件时自动填充,draft: true 默认打开——防止半成品被误发布。
图片怎么处理
这是最容易出问题的部分。我的规则是:
- 图片放在
public/img/<文章slug>/下 - 正文里用绝对路径引用:
 - 上传前用脚本压缩一遍
#!/bin/bash
# scripts/optimize-images.sh
find public/img -type f \( -name '*.png' -o -name '*.jpg' \) -newermt '-1 day' | while read -r f; do
# 用 sharp-cli 或 ImageMagick 压缩,宽度限制 1600px
npx sharp-cli -i "$f" -o "$f" resize 1600 --withoutEnlargement
done
不要在 Obsidian 里直接拖拽图片进文章,那样会生成 ![[Pasted image 20260805123456.png]] 这种 wikilink 格式,静态站点解析不了。
我的做法是在 Obsidian 设置里把「新建附件位置」指定为 public/img/当前笔记名,并把链接格式设为 Markdown 标准格式。
发布流水线
整个流程是一条命令:
#!/bin/bash
set -euo pipefail
echo "→ 检查草稿状态"
DRAFTS=$(grep -rl '^draft: true' src/content/posts/ || true)
[ -n "$DRAFTS" ] && echo "⚠️ 以下文章仍是草稿:" && echo "$DRAFTS"
echo "→ 构建"
pnpm build
echo "→ 同步到服务器"
rsync -az --delete \
--chmod=D755,F644 \
dist/ aliyun:/var/www/blog/
echo "→ 重载 Caddy"
ssh aliyun 'caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy'
echo "✓ 发布完成"
几个要点:
--delete一定要加,否则删掉的文章会残留在服务器上--chmod=D755,F644保证 Caddy(以非 root 用户运行)能读到文件- 先 validate 再 reload,配置写错时不会把线上服务搞挂
- 本地构建,别在小内存服务器上跑 Node 构建
我为什么不直接在服务器上构建
有人会问:为什么不推送到 Git,让服务器拉下来构建?
因为我的服务器只有 1.6G 内存。跑一次 Astro/Vite 构建,内存会飙到 1G 以上,加上现有的服务,很容易触发 OOM。
本地构建 + 同步产物,对个人站点来说是最省心的方案。构建耗时 8 秒,产物同步 1 秒,总共不到 10 秒。
写作流程长什么样
现在的完整流程是:
- 在 Obsidian 里新建笔记(模板自动填 frontmatter,
draft: true) - 写,随便写,中途截图直接存到
public/img/ - 写完后把
draft改成false,补上description - 终端里敲
pnpm deploy - 打开线上地址验收
整个过程不需要离开编辑器,不需要手动复制任何东西。
顺手加的一个 Python 小工具
发布前我还会跑一个脚本做体检,比肉眼扫一遍可靠。用 Python 写是因为处理字符串和文件遍历太顺手了:
#!/usr/bin/env python3
"""发布前体检:检查 frontmatter、图片引用、疑似泄漏的内部链接。"""
import re
import sys
from pathlib import Path
POSTS = Path("src/content/posts")
PUBLIC = Path("public")
FRONTMATTER = re.compile(r"\A---\n(.*?)\n---\n", re.S)
IMAGE = re.compile(r"!\[[^\]]*\]\((/[^)]+)\)")
WIKILINK = re.compile(r"\[\[[^\]]+\]\]")
def check(path: Path) -> list[str]:
problems = []
raw = path.read_text(encoding="utf-8")
m = FRONTMATTER.match(raw)
if not m:
return [f"{path.name}: 缺少 frontmatter"]
fm = m.group(1)
for field in ("title", "description", "pubDate"):
if not re.search(rf"^{field}:", fm, re.M):
problems.append(f"{path.name}: frontmatter 缺少 {field}")
if re.search(r"^draft:\s*true", fm, re.M):
problems.append(f"{path.name}: 仍是草稿 (draft: true)")
for rel in IMAGE.findall(raw):
if not (PUBLIC / rel.lstrip("/")).exists():
problems.append(f"{path.name}: 图片不存在 -> {rel}")
for w in WIKILINK.findall(raw):
problems.append(f"{path.name}: 残留 Obsidian 双链 {w}(静态站点解析不了)")
return problems
def main() -> int:
files = sorted(POSTS.glob("*.md"))
if not files:
print("没有找到文章")
return 1
all_problems = [p for f in files for p in check(f)]
for p in all_problems:
print(f"✗ {p}")
if all_problems:
print(f"\n共 {len(all_problems)} 个问题,已阻止发布")
return 1
print(f"✓ {len(files)} 篇文章检查通过")
return 0
if __name__ == "__main__":
sys.exit(main())
把它接到发布脚本最前面:
python3 scripts/preflight.py || exit 1 # 体检不过就不发布
pnpm build
那三条检查分别救过我一次:草稿忘了改、图片路径拼错、以及从 Obsidian 复制正文时带出来的 [[双链]]——最后这个如果漏了,线上就会出现原文照抄的方括号,很难看。
一点心得
降低发布摩擦,比提升写作技巧更能提高产出。
我统计过:在手动发布的那段时期,我平均每月写 1.2 篇;把流程自动化之后,稳定在每月 3–4 篇。内容质量没有下降,因为省下来的全是机械劳动。
如果你也想这么干,最小可行的版本只需要三样东西:
- 一个把 Markdown 变静态站点的生成器(Astro / Hugo / VitePress 都行)
- 一个 rsync 命令
- 一个把
draft: true拦在外面的检查
复杂度可以慢慢加,但不要为了搭流水线而推迟写第一篇文章。
本文由 Kyne 撰写,采用 CC BY-NC-SA 4.0 许可,转载请注明出处。