找回密码
 立即注册
首页 业界区 业界 Obsidian 笔记一键转换发布为 Jekyll 博客

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

咫噎 3 天前
Obsidian 是一款功能强大且灵活的知识管理和笔记软件,与 Jekyll 这一轻量级静态博客框架的结合,既能保留 Obsidian 的网状知识关联优势,又能借助 Jekyll 的高效编译能力快速生成标准化博文。
Obsidian 笔记自动转换为 Jekyll 博客一文介绍了如何把挑选出的 Obsidian 笔记转换成 Jekyll 博文保存在本地的 Jekyll 仓库中,并推送到 github/gitee,并通过webhook 部署到自己的博客服务器上。本文将在此基础上,介绍如何零成本全自动构建一站式内容生产体系。整体流程如下:

  • 用 GitHub Pages 和 Jekyll 搭建静态博客站点
  • 在 Obsidian 笔记中用 md 写笔记
  • 挑选需要作为博文发布的笔记,通过 quick add插件的 Macro 脚本把元数据写入博文清单文件
  • 运行 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 的脚本文件。
  1. module.exports = {
  2.     entry: async (params, settings) => {
  3.         const { quickAddApi,app } = params;
  4.         // 获取当前活动的文件
  5.         const activeFile = app.workspace.getActiveFile();
  6.         if (!activeFile) {
  7.             console.error('No active file found.');
  8.             return;
  9.         }
  10.         // 获取当前文件的frontmatter
  11.         const frontmatter = app.metadataCache.getFileCache(activeFile)?.frontmatter
  12.         // 获取当前文件的名称
  13.         const fileName = activeFile.basename; // 获取文件名(不含扩展名)
  14.         if(activeFile.path.indexOf(settings["blogsFolder"]) < 0) return;
  15.         // 获取当前文件的创建时间
  16.         const fileCreationTime = frontmatter.created[0] || new Date(app.workspace.getActiveFile().stat.ctime).toLocaleString().replaceAll("/","-"); // 格式化为 YYYY-MM-DD
  17.         // 获取当前文件的修改时间
  18.         const filemodifyTime = new Date(app.workspace.getActiveFile().stat.mtime).toLocaleString().replaceAll("/","-"); // 格式化为 YYYY-MM-DD
  19.         // 获取当前文件的标签
  20.         const fileTags = frontmatter?.tags || [];
  21.         // 格式化要插入的内容
  22.         const content = `## [[${fileName}]]\n`+
  23.                         `\`\`\`yaml\n`+
  24.                         `title: ${fileName}\n`+
  25.                         `date: ${fileCreationTime}\n`+
  26.                         `mtime: ${filemodifyTime}\n`+
  27.                         `categories: [${fileTags[0]}]\n`+
  28.                         `tags: [${fileTags.filter(item => item != 'blog').join(', ')}]\n`+
  29.                         `\`\`\` \n`;
  30.         // 获取或创建 list 文件
  31.         let listFile = app.vault.getAbstractFileByPath(settings["PostMetadata"]);
  32.         if (!listFile) {
  33.             return `${listFile} is not exist`;
  34.         }
  35.         let metaContent = await app.vault.read(listFile);
  36.         let reg = new RegExp(`(\\#\\# \\[\\[(`+ fileName +`)\\]\\]\n(.+\n){3}mtime:(.+)\n(.+\n){3})`,`g`);
  37.         if(!reg.test(metaContent)){
  38.             // 将内容插入到 list 文件的末尾
  39.             await app.vault.append(listFile, content + '\n');
  40.         }
  41.         else{
  42.             if(RegExp.$4.trim() != filemodifyTime){
  43.                 const newContent = metaContent.replaceAll(reg, content);
  44.                 await app.vault.modify(listFile,newContent);
  45.             }
  46.         }
  47.     },
  48.     settings: {
  49.         name: "Post_to_Jekyll configuration",
  50.         author: "czwy",
  51.         options: {
  52.             "PostMetadata": {
  53.                 type: "dropdown",
  54.                 description: "The path of Metadata file which records the article information to be saved to jekyll.",
  55.                 defaultValue: "000-Index/Posts_to_Jekyll.md",
  56.                 options: app.vault.getAllLoadedFiles().filter(item => item.extension=="md").map(item => item.path),
  57.             },
  58.             "blogsFolder": {
  59.                 type: "dropdown",
  60.                 description: "blogs folder.",
  61.                 defaultValue: "",
  62.                 options: app.vault.getAllFolders().map(item => item.path),
  63.             },
  64.         }
  65.     },
  66. };
复制代码
脚本分为 entry 和 settings 两部分, entry 是主要的业务逻辑:读取当前活动(打开的)笔记,读取笔记名称、创建时间、修改时间、标签等元数据,按照既定格式写到Posts_to_Jekyll,如果Posts_to_Jekyll没有该笔记元数据,则直接添加到末尾,如果已存在该元数据,则比较修改时间,如果修改时间不一致,则修改对应的元数据信息。
settings 是接收Quick Add 插件 Macros 脚本的设置信息,这里定义了博文类笔记保存的目录 blogsFolder 和博文元数据的清单文件 PostMetadata,在配置 Macros 时可以根据实际情况自己选择目录和文件。
1.png

将 Obsidian 笔记转换为 Jekyll 博文

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

嵌入文本块分为全文嵌入和部分嵌入,其语法如下:
  1. ![[xxx]]
  2. ![[xxx#yyy]]
  3. ![[xxx#^yyy]]
复制代码
示例中 xxx 是嵌入文本的标题,#后边是指定的文本块,如果以 ^ 开头,则是一个文本块,可以理解为一个段落 paragraph,否则表示一个标题及该级标题下所有内容。
全文嵌入的情况,只需通过正则表达式去除 front-matter 信息。
  1. return re.sub(r'---\n.*?\n---\n','',md_content,flags=re.DOTALL)
复制代码
部分嵌入文本块时,通过 MarkdownIt 的 SyntaxTreeNode 解析笔记,然后查找类型为 paragraph 且以 ^yyy 结尾的节点,读取该节点内容。
  1. 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)))
  2.                 if len(filtered) == 1:
  3.                     return '\n'+'\n'.join([child.content for child in filtered[0].children if child.type == 'text' or child.type == 'inline']).strip(target) + '\n'
  4.                 else:
  5.                     return ''
复制代码
部分嵌入标题及该级标题下所有内容时,通过 MarkdownIt 的 SyntaxTreeNode 解析笔记,然后遍历节点,找到匹配的标题时记录标题层级以及标题的行号作为起始行,然后继续遍历节点,直到找到下一个同级标题,并记录行号,将上一行作为结束行,然后读取起始行和结束行之间的内容。
  1. start_line = -1
  2. end_line = -1
  3. in_target_section = False
  4. level = -1
  5. in_target_section = False
  6. for node in root.children:
  7.         if node.type == "heading":
  8.                 title = ''.join([child.content for child in node.children if child.type == 'text' or child.type == 'inline'])
  9.                 if title.strip() == target:
  10.                         level = node.tag.replace('h', '')  # 提取标题级别
  11.                         in_target_section = True
  12.                         start_line = node.map[0]  # 起始行号
  13.                         continue
  14.                 # 遇到其他二级或更高标题时结束
  15.                 if in_target_section and int(level) <= 2:
  16.                         end_line = node.map[1] - 1  # 结束行号(前一行的末尾)
  17.                         break
  18. if start_line != -1:
  19.         lines = md_content.split('\n')
  20.         end_line = end_line if end_line != -1 else len(lines)
  21.         return '\n'+ '\n'.join(lines[start_line:end_line]).strip()+'\n'
  22. return ""
复制代码
一键发布博文

前面介绍了自动生成博文元数据清单,以及转换博文的 python 脚本,接下来需要让 Obsidian 在更新完博文元数据清单后执行 python 脚本。这里还是定义 Macros 脚本并使用 Node.js 的child_process模块执行 python 脚本。
  1. def process_obsidian_links(self):
  2.         """format url"""
  3.         def sanitize_slug(string: str) -> str:
  4.                 pattern = regex.compile(r'[^\p{M}\p{L}\p{Nd}]+', flags=regex.UNICODE)
  5.                 slug = regex.sub(pattern, '-', string.strip())
  6.                 slug = regex.sub(r'^-|-$', '', slug, flags=regex.IGNORECASE)
  7.                 return slug
  8.         """replace [[**]] to Tag """
  9.         def process_title(title, head, alias):
  10.                 return f"{(alias or title).replace('|','')}"
  11.         lines = self.content.splitlines()
  12.         new_lines = []
  13.         for i in range(len(lines)):
  14.                 # include obsidian links
  15.                 urls = re.finditer(r"\[\[(.*?)(\#.*?)?(\|.*?)?\]\]", lines[i])
  16.                 newline = ""
  17.                 pos = 0
  18.                 for url in urls:
  19.                         newline += lines[i][pos:url.start()] + process_title(url.group(1),url.group(2),url.group(3))
  20.                         pos = url.end()
  21.                 lines[i] = newline + lines[i][pos:]
  22.         self.content = '\n'.join(lines)
复制代码
entry 是 Node.js 执行 python 脚本的逻辑, settings 用于配置 python 脚本的路径,python程序的路径,以及脚本接收的参数。参数说明如下:

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

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

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

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册