Obsidian 是一款功能强大且灵活的知识管理和笔记软件,与 Jekyll 这一轻量级静态博客框架的结合,既能保留 Obsidian 的网状知识关联优势,又能借助 Jekyll 的高效编译能力快速生成标准化博文。

Obsidian 笔记自动转换为 Jekyll 博客一文介绍了如何把挑选出的 Obsidian 笔记转换成 Jekyll 博文保存在本地的 Jekyll 仓库中,并推送到 github/gitee,并通过webhook 部署到自己的博客服务器上。本文将在此基础上,介绍如何零成本全自动构建一站式内容生产体系。整体流程如下:

  1. 用 GitHub Pages 和 Jekyll 搭建静态博客站点
  2. 在 Obsidian 笔记中用 md 写笔记
  3. 挑选需要作为博文发布的笔记,通过 quick add插件的 Macro 脚本把元数据写入博文清单文件
  4. 运行 python 脚本,将对应的笔记转换成 Jekyll 博文并保存在本地的 Jekyll 仓库中并推送到 GitHub

用 GitHub Pages 搭建静态博客

GitHub 搭建博客最主流的框架是 Hugo、Jekyll、Hexo 。这里选用的是 Jekyll 的Chirpy 主题搭建博客,该主题提供了 chirpy-starter 的模板,对新手非常友好,不需要本地安装 ruby 等 Jekyll 所需要的环境,只需要把博文的 markdown 文件放到 _posts 目录,推送到 GitHub 后会自动执行 Actions 任务。详细操作参见官方文档Getting Started | Chirpy

图床

在 Obsidian 笔记中用 md 写笔记时会插入图片,通常是在 Obsidian 中配置附件目录,图片保存在本地的附件目录中,但是要把笔记发布到博客中时,这样的处理就需要额外处理图片路径,因此可以选择图床。网络上的图床方案有很多,这里选用 Cloudflare R2WebP Cloud 搭建免费图床,详细操作参考从零开始搭建你的免费图床系统(Cloudflare R2 + WebP Cloud)一文。

挑选笔记写入博文清单

Obsidian 笔记自动转换为 Jekyll 博客一文介绍了用一个单独的元数据笔记文件记录哪些笔记要转化为博文以及转化过程中需要使用的信息,但并没有描述如何自动化的生成/更新这个元数据笔记文件。我们可以借助 Quick Add 插件的 Macro 脚本功能自定义脚本读取笔记信息写到记录博文元数据的清单文件中。这里暂定清单文件名称为 Posts_to_Jekyll,并参照 QuickAdd docs 定义一个名为 WritePostMetadata.js 的脚本文件。

module.exports = {
    entry: async (params, settings) => {
        const { quickAddApi,app } = params;
        // 获取当前活动的文件
        const activeFile = app.workspace.getActiveFile();
        if (!activeFile) {
            console.error('No active file found.');
            return;
        }
        // 获取当前文件的frontmatter
        const frontmatter = app.metadataCache.getFileCache(activeFile)?.frontmatter
        // 获取当前文件的名称
        const fileName = activeFile.basename; // 获取文件名(不含扩展名)
        if(activeFile.path.indexOf(settings["blogsFolder"]) < 0) return;
        // 获取当前文件的创建时间
        const fileCreationTime = frontmatter.created[0] || new Date(app.workspace.getActiveFile().stat.ctime).toLocaleString().replaceAll("/","-"); // 格式化为 YYYY-MM-DD
        // 获取当前文件的修改时间
        const filemodifyTime = new Date(app.workspace.getActiveFile().stat.mtime).toLocaleString().replaceAll("/","-"); // 格式化为 YYYY-MM-DD
        // 获取当前文件的标签
        const fileTags = frontmatter?.tags || [];
        // 格式化要插入的内容
        const content = `## [[${fileName}]]\n`+
                        `\`\`\`yaml\n`+
                        `title: ${fileName}\n`+
                        `date: ${fileCreationTime}\n`+
                        `mtime: ${filemodifyTime}\n`+
                        `categories: [${fileTags[0]}]\n`+
                        `tags: [${fileTags.filter(item => item != 'blog').join(', ')}]\n`+
                        `\`\`\` \n`;
        // 获取或创建 list 文件
        let listFile = app.vault.getAbstractFileByPath(settings["PostMetadata"]);
        if (!listFile) {
            return `${listFile} is not exist`;
        }
        let metaContent = await app.vault.read(listFile);
        let reg = new RegExp(`(\\#\\# \\[\\[(`+ fileName +`)\\]\\]\n(.+\n){3}mtime:(.+)\n(.+\n){3})`,`g`);
        if(!reg.test(metaContent)){
            // 将内容插入到 list 文件的末尾
            await app.vault.append(listFile, content + '\n');
        }
        else{
            if(RegExp.$4.trim() != filemodifyTime){
                const newContent = metaContent.replaceAll(reg, content);
                await app.vault.modify(listFile,newContent);
            }
        }
    },     settings: {
        name: "Post_to_Jekyll configuration",
        author: "czwy",
        options: {
            "PostMetadata": {
                type: "dropdown",
                description: "The path of Metadata file which records the article information to be saved to jekyll.",
                defaultValue: "000-Index/Posts_to_Jekyll.md",
                options: app.vault.getAllLoadedFiles().filter(item => item.extension=="md").map(item => item.path),
            },
            "blogsFolder": {
                type: "dropdown",
                description: "blogs folder.",
                defaultValue: "",
                options: app.vault.getAllFolders().map(item => item.path),
            },
        }
    },
};

脚本分为 entrysettings 两部分, entry 是主要的业务逻辑:读取当前活动(打开的)笔记,读取笔记名称、创建时间、修改时间、标签等元数据,按照既定格式写到Posts_to_Jekyll,如果Posts_to_Jekyll没有该笔记元数据,则直接添加到末尾,如果已存在该元数据,则比较修改时间,如果修改时间不一致,则修改对应的元数据信息。

settings 是接收Quick Add 插件 Macros 脚本的设置信息,这里定义了博文类笔记保存的目录 blogsFolder 和博文元数据的清单文件 PostMetadata,在配置 Macros 时可以根据实际情况自己选择目录和文件。

将 Obsidian 笔记转换为 Jekyll 博文

Obsidian 笔记自动转换为 Jekyll 博客一文介绍了 Obsidian 笔记转换为 Jekyll 博文时需要处理的一些细节:博文日期、图片处理、链接处理、Callouts 转换为 Prompts,并提供了Python 脚本文件。在我日常笔记应用中会使用到 wiki 链接[[]] 和嵌入文本块![[]],因此在原有脚本基础上增加了这两类语法的处理。

处理嵌入文本块

嵌入文本块分为全文嵌入和部分嵌入,其语法如下:

![[xxx]]
![[xxx#yyy]]
![[xxx#^yyy]]

示例中 xxx 是嵌入文本的标题,#后边是指定的文本块,如果以 ^ 开头,则是一个文本块,可以理解为一个段落 paragraph,否则表示一个标题及该级标题下所有内容。

全文嵌入的情况,只需通过正则表达式去除 front-matter 信息。

return re.sub(r'---\n.*?\n---\n','',md_content,flags=re.DOTALL)

部分嵌入文本块时,通过 MarkdownItSyntaxTreeNode 解析笔记,然后查找类型为 paragraph 且以 ^yyy 结尾的节点,读取该节点内容。

filtered = list(map(lambda r:r,filter(lambda node: node.type == "paragraph" and ''.join([child.content for child in node.children if child.type == 'text' or child.type == 'inline']).endswith(target), root.children)))

                if len(filtered) == 1:

                    return '\n'+'\n'.join([child.content for child in filtered[0].children if child.type == 'text' or child.type == 'inline']).strip(target) + '\n'

                else:

                    return ''

部分嵌入标题及该级标题下所有内容时,通过 MarkdownItSyntaxTreeNode 解析笔记,然后遍历节点,找到匹配的标题时记录标题层级以及标题的行号作为起始行,然后继续遍历节点,直到找到下一个同级标题,并记录行号,将上一行作为结束行,然后读取起始行和结束行之间的内容。

start_line = -1
end_line = -1
in_target_section = False level = -1
in_target_section = False
for node in root.children:
if node.type == "heading":
title = ''.join([child.content for child in node.children if child.type == 'text' or child.type == 'inline'])
if title.strip() == target:
level = node.tag.replace('h', '') # 提取标题级别
in_target_section = True
start_line = node.map[0] # 起始行号
continue
# 遇到其他二级或更高标题时结束
if in_target_section and int(level) <= 2:
end_line = node.map[1] - 1 # 结束行号(前一行的末尾)
break if start_line != -1:
lines = md_content.split('\n')
end_line = end_line if end_line != -1 else len(lines)
return '\n'+ '\n'.join(lines[start_line:end_line]).strip()+'\n'
return ""

需要注意的是,提取的嵌入式文本可能也嵌入了其他的笔记,因此需要递归执提取。详细的脚本代码见czwy/obsidian-to-jekyll: A simple python script that converts Obsidian notes to Jekyll themes, and deploy to github pages.

处理 wiki 链接

首先需要说明的是,这里介绍的 wiki 链接处理思路局限性非常大,只是将[[]]的内容转换为 <a>标签,链接的文本必须是也作为博客发布的笔记,否则 Github 执行 Action 时会因为找到不链接导致构建失败。处理的脚本如下:

def process_obsidian_links(self):
"""format url"""
def sanitize_slug(string: str) -> str:
pattern = regex.compile(r'[^\p{M}\p{L}\p{Nd}]+', flags=regex.UNICODE)
slug = regex.sub(pattern, '-', string.strip())
slug = regex.sub(r'^-|-$', '', slug, flags=regex.IGNORECASE)
return slug
"""replace [[**]] to Tag <a>"""
def process_title(title, head, alias):
return f"<a href=\"/posts/{sanitize_slug(title.lower())}/{head or ''}\">{(alias or title).replace('|','')}</a>"
lines = self.content.splitlines()
new_lines = []
for i in range(len(lines)):
# include obsidian links
urls = re.finditer(r"\[\[(.*?)(\#.*?)?(\|.*?)?\]\]", lines[i])
newline = ""
pos = 0
for url in urls:
newline += lines[i][pos:url.start()] + process_title(url.group(1),url.group(2),url.group(3))
pos = url.end()
lines[i] = newline + lines[i][pos:]
self.content = '\n'.join(lines)

一键发布博文

前面介绍了自动生成博文元数据清单,以及转换博文的 python 脚本,接下来需要让 Obsidian 在更新完博文元数据清单后执行 python 脚本。这里还是定义 Macros 脚本并使用 Node.js 的child_process模块执行 python 脚本。

module.exports = {
entry: async (params, settings) => {
const { quickAddApi,app,obsidian } = params; const { exec } = require('child_process');
const { promisify } = require('util');
const fs = require('fs');
const path = require('path');
const os = require('os');
const execAsync = promisify(exec); try { let listFile = app.vault.getAbstractFileByPath(settings["PythonScript"]);
const scriptPath = path.join(app.vault.adapter.basePath,listFile.path);
const setEncoding = process.platform === 'win32' ? 'chcp 65001 > nul && ' : ''; const execPath = settings["execPath"] || "python";
const params = settings["parameters"];
const command = `${setEncoding}"${execPath}" -u "${scriptPath}" ${params}`;
const { stdout, stderr } = await execAsync(command, {
timeout: 30000,
encoding: 'utf8',
env: {
...process.env,
PYTHONIOENCODING: 'utf-8'
}
});
new obsidian.Notice(stdout || stderr || '代码执行完成,无输出',3000);
return stdout || stderr || '代码执行完成,无输出';
} catch (error) {
return `执行错误:${error.message}`;
}
},
settings: {
name: "Post_to_Jekyll configuration",
author: "czwy",
options: {
"PythonScript": {
type: "dropdown",
description: "The path of python script",
defaultValue: "088-Template/Script/obsidian_to_jekyll.py",
options: app.vault.getAllLoadedFiles().filter(item => item.extension=="py").map(item => item.path),
},
"execPath": {
type: "text",
defaultValue: "",
placeholder: "Placeholder",
description: "the path of python",
},
"parameters": {
type: "text",
defaultValue: "-w",
placeholder: "Placeholder",
description: " arguments for Script.",
},
}
},
};

entry 是 Node.js 执行 python 脚本的逻辑, settings 用于配置 python 脚本的路径,python程序的路径,以及脚本接收的参数。参数说明如下:

  • -w:把笔记转换为Jekyll 博文并保存在本地的 Jekyll 仓库中
  • -c:提交修改
  • -p:把修改push到GitHub



    至此,主要工作都已完成,接下来就是组合 Macros 脚本,在 QuickAdd 的设置界面中添加一个名为 Post-to-Jekyll的 macro,然后在 Post-to-Jekyll的设置中的User Scripts中依次选用 WritePostMetadata.jsexecPython.js,并在脚本中间插入 100ms 的等待。



    当写完博文需要发布时,只需要打开要发布的博文,用 Ctrl+P 调出命令列表,执行 Post-to-Jekyll命令(也可以为该命令配置快捷键)就可以一键发布博文到 GitHub Pages 了。

Obsidian 笔记一键转换发布为 Jekyll 博客的更多相关文章

  1. 搭建jekyll博客

    使用jekyll将markdown文件生成静态的html文件,并使用主题有序的进行布局,形成最终的博客页面. 特点 基于ruby 使用Markdown书写文章 无需数据库 可以使用GitHub Pag ...

  2. Github pages + jekyll 博客快速搭建

    Github pages + jekyll 博客快速搭建 寻找喜欢的模版 https://github.com/jekyll/jekyll/wiki/sites http://jekyllthemes ...

  3. (转) OpenCV学习笔记大集锦 与 图像视觉博客资源2之MIT斯坦福CMU

          首页 视界智尚 算法技术 每日技术 来打我呀 注册     OpenCV学习笔记大集锦 整理了我所了解的有关OpenCV的学习笔记.原理分析.使用例程等相关的博文.排序不分先后,随机整理的 ...

  4. 我的Jekyll博客

    我在GitHub Page上托管的Jekyll博客地址:http://lastavenger.github.io/

  5. 在Jekyll博客添加评论系统:gitment篇

    最近在Github Pages上使用Jekyll搭建了个人博客( jacobpan3g.github.io/cn ), 当需要添加评论系统时,找了一下国内的几个第三方评论系统,如"多说&qu ...

  6. Word直接发布新浪博客(以Wo…

    原文地址:Word直接发布新浪博客(以Word 2013为例)作者:paulke2011 注意:这篇博客直接由Word 2013发出!这虽然也算是一个教程,但更多的是一个试验品. 老早就知道Word有 ...

  7. [转载]Word直接发布新浪博客(以Word 2013为例)

    原文地址:Word直接发布新浪博客(以Word 2013为例)作者:paulke2011 注意:这篇博客直接由Word 2013发出!这虽然也算是一个教程,但更多的是一个试验品. 老早就知道Word有 ...

  8. Jekyll博客添加Valine评论

    Jekyll博客添加Valine评论 关于github搭建jekyl博客,在这里不做过多描述,详情参考: 百度搜索关键字:github搭建jekyll博客 官网:https://www.jekyll. ...

  9. 在GitLab pages上快速搭建Jekyll博客

    前一段时间将我的Jekyll静态博客从github pages镜像部署到了 zeit.co(现vercel)上了一份,最近偶然发现gitlab pages也不错,百度也会正常抓取,于是动手倒腾,将gi ...

  10. Docsify+腾讯云对象存储 COS,一键搭建云上静态博客

    最近一直在想如何利用 COS 简化静态博客的搭建过程.搜了很多的静态博客搭建过程,发现大部分的静态博客都要通过编译才能生成静态页面.功夫不负有心人,终于让我找到了一个超简洁博客的搭建方法. 效果预览 ...

随机推荐

  1. 如何像专家一样高效使用 Google 搜索

    如何像专家一样高效使用 Google 搜索 你几乎可以在互联网上搜索到任何内容,而Google是大多数人选择搜索信息的主要途径之一. 尽管频繁地使用Google,但是大部分互联网用户都不知道如何快速和 ...

  2. 微服务实战系列(十一)-微服务之自定义脚手架-copy

    微服务实战系列(十一)-微服务之自定义脚手架   1. 场景描述 (1)随着微服务越来越常见,一个大的项目会被拆分成多个小的微服务,jar包以及jar之间的版本冲突问题,变得越来越常见,如何保持整体微 ...

  3. Spring Security + Redis + JWT 实现动态权限管理【前后端分离】

    本篇文章环境:Spring Boot + Mybatis + Spring Security + Redis + JWT 数据库设计Web 的安全控制一般分为两个部分,一个是认证,一个是授权.认证即判 ...

  4. neo4j存储数据-图数据库

    1. 简介 本文主要介绍neo4j是如何将图数据保存在磁盘上的,采用的是什么存储方式.分析这种存储方式对进行图查询/遍历的影响. 2. 图数据库简介 生产环境中使用的图数据库主要有2种,分别是带标签的 ...

  5. MVCC基本原理

    在介绍MVCC概念之前,我们先来想一下数据库系统里的一个问题:假设有多个用户同时读写数据库里的一行记录,那么怎么保证数据的一致性呢?一个基本的解决方法是对这一行记录加上一把锁,将不同用户对同一行记录的 ...

  6. Ubuntu更改用户名

    网上给出Ubuntu更改用户名步骤: 1.进入Ubuntu,打开一个终端,输入 sudo su转为root用户. 注意,必须先转为root用户!!! 2.gedit /etc/passwd ,找到代表 ...

  7. Luogu P2414 NOI2011 阿狸的打字机 题解 [ 紫 ] [ AC 自动机 ] [ 离线思想 ] [ 树状数组 ] [ dfs 序 ]

    阿狸的打字机:非常牛的 AC 自动机题. 暴力 先考虑在暴力的情况下,我们如何计算 \(x\) 匹配 \(y\) 的次数.显然,我们会模拟往 \(y\) 里加字符的过程,在此过程中做 KMP 进行匹配 ...

  8. 为什么TCP需要三次握手?深入解析背后的设计哲学

    在互联网通信中,TCP(传输控制协议)是确保数据可靠传输的基石.而TCP连接的建立过程--"三次握手"(Three-Way Handshake),看似简单的三个步骤,却蕴含了网络协 ...

  9. MOS管选型

    MOS管基本参数 MOS管(Metal-Oxide-Semiconductor Field-Effect Transistor, MOSFET)作为开关元件的应用非常广泛,其开关特性与三极管相比有所不 ...

  10. autMan奥特曼机器人-实时翻译的用法

    一.基本配置 访问并登录百度翻译开放平台:https://api.fanyi.baidu.com/ 进入开发者信息获取 APP ID和密钥,并开通"通用文本翻译"服务 autMan ...