从 Obsidian 到 Astro,我的懒人同步方案

1750 字
9 分钟
从 Obsidian 到 Astro,我的懒人同步方案
Abstract

自从把博客从 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 AST
  • rehype-figure.mjs 负责最后把图片包装成 figure + figcaption

先说接入。这个顺序其实挺关键的,我这里把 remarkObsidianLink 放在前面,不然后面的 remarkImageGridremarkExcerpt 拿到的还是原始 [[...]] 文本。

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-captiondata-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]]
  • ![说明|150](image.jpg)

当然也不是全都做了。像 ![[某篇文章]] 这种非图片嵌入,或者 [[文章^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 提交一条龙”的脚本。这个插件可玩性很高,你可以找到你喜欢的玩法。

文章分享

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

从 Obsidian 到 Astro,我的懒人同步方案
https://blog.idotcar.top/posts/obsdian-sync-astro/
作者
老鼠溺水
发布于
2026-04-07
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
老鼠溺水
事实上,我们每个人都不过是在给自己写信。
分类
标签
站点统计
文章
7
分类
2
标签
9
总字数
29,837
运行时长
0
最后活动
0 天前

目录