Obsidian 笔记一键转换发布为 Jekyll 博客

news/2025/3/28 15:01:07/文章来源:https://www.cnblogs.com/czwy/p/18791110

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 R2 和 WebP 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 时可以根据实际情况自己选择目录和文件。
WritePostMetadataSetting

将 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 = Falselevel = -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 = Truestart_line = node.map[0]  # 起始行号continue# 遇到其他二级或更高标题时结束if in_target_section and int(level) <= 2:end_line = node.map[1] - 1  # 结束行号(前一行的末尾)breakif 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 linksurls = re.finditer(r"\[\[(.*?)(\#.*?)?(\|.*?)?\]\]", lines[i])newline = ""pos = 0for 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
    Post_to_Jekyll_configuration
    至此,主要工作都已完成,接下来就是组合 Macros 脚本,在 QuickAdd 的设置界面中添加一个名为 Post-to-Jekyll的 macro,然后在 Post-to-Jekyll的设置中的User Scripts中依次选用 WritePostMetadata.jsexecPython.js,并在脚本中间插入 100ms 的等待。
    Post_to_Jekyll_Macro
    当写完博文需要发布时,只需要打开要发布的博文,用 Ctrl+P 调出命令列表,执行 Post-to-Jekyll命令(也可以为该命令配置快捷键)就可以一键发布博文到 GitHub Pages 了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/905387.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

变更《营业执照》操作流程

第一步:打开甘肃政务服务网 https://zwfw.gansu.gov.cn/ 第二步:登录 (1)右上角(2)点“法人登录”(3)点“电子营业执照登录”第三步:使用【电子营业执照】的【扫一扫】进行登录 (1)打开法人的手机微信,在微信中搜索“电子营业执照小程序”(2)点击“扫一扫”(3)…

DVWA靶场安装教程

1 靶场下载github 下载 https://github.com/digininja/DVWAgithub网站在国外,有时不能访问,可以下载我分享的这个:百度网盘分享 https://pan.baidu.com/s/1vIsf_VFiY9Ah3DG3Ichsyg?pwd=zyvf 2 靶场部署 2.1 解压缩靶场 解压缩后,只保留DVWA-master文件夹,里面是靶场代码…

leaflet框选范围下载地图离线瓦片:以高德地图为例(附源码下载)

demo源码运行环境以及配置运行环境:依赖Node安装环境,demo本地Node版本:14.19.1。 运行工具:vscode或者其他工具。 配置方式:下载demo源码,vscode打开,然后顺序执行以下命令: (1)下载demo环境依赖包命令:npm i (2)启动Node后端接口命令:node nodeServer.js (3)打…

Python 也能做前端?用 Streamlit + LangChain 搭建 AI Chat 应用!

项目成果 使用 LangChain 处理对话逻辑,包括消息存储、上下文管理。 使用 Streamli项目成果使用 LangChain 处理对话逻辑,包括消息存储、上下文管理。 使用 Streamlit 构建前端 UI,实现流式输出对话体验。 调用 OpenAI API 进行智能问答,使 AI 能够自然交互。 支持对话历史…

春雨

在某一瞬间抛下了自己的灵魂,任由祂对着死去的肉体宣泄、嘶吼、哀悼。 折断笔不去书写过往,可回想的每一刻都值得记录。 工作、学习、人际关系、比赛、生活, 或者说焦虑、压力、责任、情感、不甘, 为什么不想想,把生活放到第一位。 在这苦难中,对知识的渴望和情感的向往刺…

leetcode每日一题:对角线上不同值的数量差

题目 2711. 对角线上不同值的数量差 给你一个下标从 0 开始、大小为 m x n 的二维矩阵 grid ,请你求解大小同样为 m x n 的答案矩阵 answer 。 矩阵 answer 中每个单元格 (r, c) 的值可以按下述方式进行计算:令 topLeft[r][c] 为矩阵 grid 中单元格 (r, c) 左上角对角线上 不…

Linux基本功

知识来源:B站-尚硅谷-武晟然 https://www.bilibili.com/video/BV1WY4y1H7d3/?spm_id_from=333.337.search-card.all.click&vd_source=d9e91669cbb2a200e91188c38bf38a7a 第01章_常用基本命令 ​ Shell 可以看作是一个命令解释器,为我们提供了交互式的文本控制台界面。我…

宝塔部署的Java项目通过域名无法访问问题

背景 1.域名已经绑定IP 2.端口已经开放,宝塔和云服务器的端口设置都开放 3.测试通过公网IP:port可以访问 4.通过域名:port不可以访问 猜测1: Java 项目未绑定到0.0.0.0 Java 项目默认监听 127.0.0.1(仅允许本地访问),无法通过公网 IP 或域名访问。 解决方案:启动 Java 项目…

知识库管理:全流程智能化中枢,驱动企业信息资产高效流转

思通数科智能系统的知识库管理模块,以多模态数据管理为基础,深度融合AI技术与精细化流程控制,构建从内容创建、智能分类到版本追溯的全生命周期管理体系,助力企业实现知识资产的安全存储、高效利用与持续优化。以下是核心功能详解: 一、多模态数据统一存储:打破信息孤岛,…

IPD流程中的风险管理与应对方法

IPD(Integrated Product Development)流程即集成产品开发流程,是一套产品开发的模式、理念与方法。它强调将产品开发视为一个完整的流程,涵盖从市场需求分析、产品规划、设计开发到产品上市及后续维护等各个环节。在当今复杂多变的商业环境中,IPD流程对于企业提升产品竞争…

9.0预览文件失败, 提示需要创建FileUpLoadsTempfilePath并授权

原因: 程序目录下已存在的FileUpLoadTempfilePath对比路径缺少了s, 正确为FileUpLoadsTempfilePath, 如下图.

如何设计一个便携式温度计- NFC/ QT PY/ SHT40

无论你走到哪里,检查周围的温度和湿度,这是一个紧凑的无电池设计! 智能手机虽然功能齐全,功能强大,但由于其设计和主要用途,它并不是理想的温度计。它们的内部温度传感器是用来监测设备性能的,而不是环境条件,它们会受到手机本身和周围环境产生的热量的影响,导致读数不…