从 Obsidian 到 Astro,我的懒人同步方案
自从把博客从 NotionNext 迁移到 Astro 之后,需要考虑的就是同步方案了。之前基于 NotionNext 同步方案基本上是先同步到 Notion,再发布,该方案需要解决图床的问题,现在使用 Astro,基本不要考虑图床,只要 Markdown 格式维持对图片路径的相对引用即可。我使用的方案为插件配合使用 Github Action同步。
第一步:解决图片路径(Custom Attachment Location)
我的诉求很简单:发博客的图片统一塞到 Astro博客/images/ 目录下,而不是跟着每篇笔记乱跑。但 Obsidian 默认的附件逻辑是全局的,我不想为了博客把整个库的图片都搞乱。
这里推荐用 Custom Attachment Location 插件,配合自定义 Token 就能实现“分而治之”。

先去插件设置里注册一个 smartPath 令牌:
registerCustomToken('smartPath', (ctx) => { // 1. 指定你的博客目录名 const specialFolder = 'Astro博客';
// 2. 如果笔记在博客目录下,附件就丢到该目录根部的 images 文件夹 if (ctx.noteFolderPath.startsWith(specialFolder)) { return specialFolder + '/images/' + ctx.noteFileName; }
// 3. 库里其他笔记还是走原来的逻辑,互不干扰 return './assets/' + ctx.noteFileName;});
然后把插件的“新附件位置”改成 ${smartPath} 即可。这样我在博客目录下插图,它会自动按文章名建文件夹存图,非常省心。

第二步:配置 GitHub Action 搬运工
这一步是实现自动化的核心。前提是你已经装好了 Obsidian Git 插件,且 Obsidian 仓库和 Astro 仓库是分开的。
先去仓库设置里生成一个带 repo 权限的 GH_TOKEN,填到 Secrets 里。

Action 的思路是:监听到博客目录有变动,就拉取 Astro 仓库,用 rsync 把内容同步过去,最后自动提交。
name: Obsidian To Astro Sync
on: push: branches: - main paths: - 'Astro博客/**' # 只监听博客目录,别的文件改了不触发 workflow_dispatch:
env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs: sync-job: runs-on: ubuntu-latest steps: - name: Checkout Obsidian (Sparse) uses: actions/checkout@v4 with: sparse-checkout: | Astro博客 sparse-checkout-cone-mode: false
- name: Checkout Astro Project uses: actions/checkout@v4 with: repository: wuqicyber/astro-firefly # 换成你的仓库名 token: ${{ secrets.GH_TOKEN }} path: astro-repo
- name: Sync and Transform run: | SOURCE_DIR="Astro博客" DEST_DIR="astro-repo/src/content/posts"
mkdir -p "$DEST_DIR" # 用 rsync --delete 保证两边文件同步,删掉本地没有的旧文章 rsync -av --delete "$SOURCE_DIR/" "$DEST_DIR/"
- name: Commit and Push working-directory: ./astro-repo run: | git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com"
git add . if ! git diff --quiet --exit-code --cached; then git commit -m "chore: sync content from Obsidian vault ($(date +'%Y-%m-%d %H:%M'))" git push origin master else echo "No changes, skipping." fi注意点: 确认好两个仓库的分支名(main 还是 master),别推错了。
第三步: Obsidian 双链格式兼容
比较麻烦的是这一步。
Obsidian 默认的双链格式不是标准的Markdown(图片我用了 Image Captions 插件):
[[文章名]][[文章名#某个标题]]![[图片.jpg]]![[图片.jpg|说明文字|right|150]]
可以使用脚本或者插件把这种格式转换为Markdown格式,考虑到Obsidian 底层也是用Remark做的双链,我决定直接在 Astro 里面兼容 Obsidian 的双链。
主要拆成了两步:
remark-obsidian-link.js负责先把 Obsidian 语法翻译成正常的 Markdown ASTrehype-figure.mjs负责最后把图片包装成figure + figcaption
先说接入。这个顺序其实挺关键的,我这里把 remarkObsidianLink 放在前面,不然后面的 remarkImageGrid、remarkExcerpt 拿到的还是原始 [[...]] 文本。
markdown: { remarkPlugins: [ remarkMath, remarkReadingTime, remarkObsidianLink, remarkImageGrid, remarkExcerpt, ], rehypePlugins: [ rehypeSlug, rehypeMermaid, rehypeFigure, ],}remark-obsidian-link.js 这边干的第一件事,就是先把 src/content/posts 扫一遍,顺手建好文章和图片的索引。这个很重要,因为我写双链的时候并不想管文件到底在第几层目录。
比如我正文里写的是:
[[guide]]![[file-20260407144622571.jpg]]插件会先去索引里查:
guide对应的是哪篇文章file-20260407144622571.jpg对应的是哪张图
图片这块我额外加了一层优先规则:如果只是裸文件名,它会先去 images/<当前笔记名>/ 下面找。因为我前面就是这么存图的,所以这一步必须自己接。
这一块真正干活的是 buildObsidianImageNode(),逻辑差不多是这样:
const resolvedImage = resolveImageTarget( target, currentNoteFileName, indexes,);
let relativeUrl = toPosix( path.relative(path.dirname(currentFilePath), resolvedImage.filePath),);
return applyImagePresentation( { type: "image", url: relativeUrl, alt: "", }, parseImagePresentation(optionSegments, target, indexes),);说白了,这一步就是先找到图,再把 |说明|right|150 这一串参数拆出来,一起挂到图片节点上。
parseImagePresentation() 这一层主要处理的就是图片说明插件那套语法:
- 单个数字,比如
|150,就当宽度 |100x145这种就当宽高|left、|right、|center这几个就当对齐- 其他普通文本就当 caption
%和%.%这种占位符,也是在这一步处理掉
所以像下面这种写法:
![[image.jpg|这是一条图片说明|right|150]]到 Remark 这里已经不是一段普通字符串了,而是“图片路径 + caption + 对齐 + 尺寸”都拆好的结构。
接下来 rehype-figure.mjs 再收尾。它会去读前面塞进去的 data-figure-caption 和 data-figure-align,然后把单个 <img> 包成标准的 figure 结构:
const figureNode = { type: "element", tagName: "figure", properties: { className: [ "image-captions-figure", align ? `image-captions-align-${align}` : "image-captions-align-center", ], }, children: figureChildren,};最后出来的 HTML 基本就是这个样子:
<figure class="image-captions-figure image-captions-align-right"> <img src="..." width="150" /> <figcaption class="image-captions-caption">这是一条图片说明</figcaption></figure>到这里图片说明、宽度、左右浮动其实就都齐了,剩下只是样式问题。我这里又在 markdown.css 里补了 .image-captions-figure 和 .image-captions-caption 的样式,让它至少看起来像那么回事,而不是只在结构上“解析成功”。
这一套下来,我自己现在常用的这些都已经能正常发:
[[文章名]][[文章名#标题]]![[图片.jpg]]![[图片.jpg|说明]]![[图片.jpg|说明|150]]![[图片.jpg|说明|left/right/center]]
当然也不是全都做了。像 ![[某篇文章]] 这种非图片嵌入,或者 [[文章^block]] 这种块引用,我现在还是没接。原因也很简单,这些东西要么不常用,要么处理错了特别难查。我宁可先保留原文,再给 warning,也不想硬生生生成一个看起来像对、实际上是错的东西。
对我来说,到这一步就够用了。因为我真正高频的需求其实就三个:
- 双链别丢
- 图片别炸
- 图片说明和尺寸别失效
这三个打通之后,Obsidian 写作体验基本就能原样搬到 Astro 上了。
一个测试引用:建站记录
其他:Templater 和 QuickAdd
Templater
我用它来生成标准的 Frontmatter 模板。新建文章时点一下,标题、日期、Slug 全部自动生成。
---title: <% tp.file.title %>published: <% tp.date.now("YYYY-MM-DD") %>updated: <% tp.date.now("YYYY-MM-DD") %>description: <% tp.file.title %>的摘要slug: |- <%* function randomSlug(length = 8) { const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; let result = ""; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } tR += result; } randomSlug(); %>tags: - 随笔category: 技术记录draft: true---QuickAdd
比如可以配置“Git 提交一条龙”的脚本。这个插件可玩性很高,你可以找到你喜欢的玩法。

文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!