SouthFox's Garden

Search IconA magnifying glass icon. 搜索
种植日期: 2022-08-30 上次照料: 2025-06-02

来把 org-roam 笔记发布出去吧

org-roam 很好,不过笔记积累久了就会产生分享欲,想要在线发布出去……最近摸爬滚打几天终于是弄出来了……

方案选择

首先先去网上搜索了一遍,发现还是有一些前人踩路的……

  • Neil’s Digital Garden 这个网页弄得很漂亮,完成度也很高,然后具体一看源代码,通篇 JavaScript ……额,倒不是说 JS 坏话啦,不过我现在没打算在学 JS 的, 而且 npm 确实很折磨人。 25 EDIT: 研究过后发现只是一点简单的在浏览器运行的 js (just javascript) 脚本来调用他人预先写好的功能,不知道当初我为什么会觉得很难……

    How I publish my wiki with org-publish 所使用的代码也确实有点复杂了(我,纯纯菜狐)。

  • My Org Roam Notes Workflow - Hugo Cisneros 这篇文章讲的也不错,不过其博客主题源代码并没有和 org-roam 相关的,好像只有一些框架。好吧,也知道弄 Emacs 相关的作业没那么好抄的,心理准备还是有的。

    不过这个博客讲了如何使用 Python 分析 org-roam 的数据库解析出关系图谱,但我现在的想法是只把我一部分笔记发布出去,而我又想通过 Drone 这个 CI 平台自动构建网站,所以也用不到了……

  • 最后试了试 jethrokuan/braindump 方案,这个也是 org-roam 作者在用的,所以也算是钦定方案了(

    但实际其采用的脚本我无法使用,可能和我用的 Doom Emacs batch 环境有问题吧,也不知道怎么解决(我,究极菜狐)……所以最后只能自己慢慢折腾了!

准备

首先要先下载 ox-hugo 包,先把 org-mode 所用的 .org 文件转换成 .md 再给 hugo 解析……没错,Emacser 就是这么自由,虽说 hugo 原生就支持 org 文件,不过 Markdown 支持更优秀的话用 Markdown 也不是不行……(反正也是自动转换的)。

代码

(setq org-hugo-base-dir "~/Documents/roam-publish/")

(defun my/org-roam-filter-by-tag (tag-name)
  (lambda (node)
    (member tag-name (org-roam-node-tags node))))

(defun my/org-roam-list-notes-by-tag (tag-name)
  (mapcar #'org-roam-node-file
          (seq-filter
           (my/org-roam-filter-by-tag tag-name)
           (org-roam-node-list))))

(defun my/org-roam-export-all ()
  "Re-exports all Org-roam files to Hugo markdown."
  (interactive)
  (dolist (org-file (my/org-roam-list-notes-by-tag "publish"))
    (with-current-buffer (find-file org-file)
        (my/org-roam-publish t))))

(defun my/insert--backlinks (&optional heading-func)
  (if-let ((backlinks (org-roam-backlinks-get (org-roam-node-at-point) :unique t)))
      (save-excursion
        (when heading-func
          (funcall heading-func))
        (insert " 反向链接\n")
        (dolist (backlink backlinks)
          (let* ((source-node (org-roam-backlink-source-node backlink))
                 (point (org-roam-backlink-point backlink)))
            (insert
             (format "- [[id:%s][%s]]\n"
                     (org-roam-node-id source-node)
                     (org-roam-node-title source-node))))))))

(defun my/insert-backlinks ()
  (goto-char (point-min))
  (org-shifttab 0)
  (while (= 0 (org-next-visible-heading 1))
    (when (assoc "ID" (org-entry-properties))
      (my/insert--backlinks
       (lambda ()
         (org-insert-subheading t)))))
  (goto-char (point-min))
  (my/insert--backlinks
   (lambda ()
     (goto-char (point-max))
     (insert "* "))))

(defun my/extract-org-roam-id (origin-filename)
  (save-excursion
    (goto-char (point-min))
    (let ((matche '()))
      (while (re-search-forward "\\[\\[id:\\(.*?\\)]\\[.*?\\]\\]" nil t)
        (when-let* ((uuid (match-string-no-properties 1))
                    (roam-node (org-roam-node-from-id uuid))
                    (file-name (car (org-roam-id-find uuid))))
          (when (member "publish" (org-roam-node-tags roam-node))
            (push file-name matche))))
      (delete origin-filename (seq-uniq matche)))))

(defun my/org-roam-publish (&optional skip-publish skip-update-backlinks)
  "Publish current file"
  (interactive)
  (goto-char (point-min))
  (org-roam-update-org-id-locations)
  (org-roam-tag-add (list "publish"))
  (unless skip-update-backlinks
    (org-roam-set-keyword "hugo_lastmod" (format-time-string "%Y-%m-%d %H:%M:%S")))
  (save-buffer)
  (let ((publish-content (buffer-string))
        (origin-filename (buffer-file-name))
        (filename (concat (file-name-base (buffer-file-name)) ".org")))
    (with-temp-buffer
      (erase-buffer)
      (insert publish-content)
      (org-mode)
      (org-roam-set-keyword "EXPORT_FILE_NAME" filename)
      (org-roam-tag-remove (list "publish"))
      (my/insert-backlinks)
      (unless skip-update-backlinks
        (let ((back-files (my/extract-org-roam-id origin-filename)))
          (dolist (back-file back-files)
            (message back-file)
            (with-current-buffer (get-file-buffer back-file)
              (my/org-roam-publish t t)))))
      (org-hugo-export-wim-to-md)))
  (unless skip-publish
    (async-shell-command (concat
                          "cd " org-hugo-base-dir
                          " && " "git add ."
                          (if (yes-or-no-p "Push now?")
                              (concat " && " "git commit -m '[post] new post'"
                                      " && " "git push"))) "*Messages*"))
  (message "publish new post!"))

最后一个函数就是完全自己写的了,完全没有技术含量,无非就是把一些函数堆叠起来,不过真心好用 …… 论 Emacs 的易折腾性

25 EDIT: 经过几年发展也算是有点技术含量了,更懂该堆什么函数了,之前用的方案还要用 python 解析 hugo 生成出来的 html 文件插入反链,但是这次一看其实 org-roam 包默认就很方便拿出反链。现在方案是只对单个文件进行发布然后更新相应的反链和元数据,将 ox-hugo 生成的 markdown 文件作为一个中间层(世界法则,遇事不决加中间层)将需要改动的量压到最小,不便之处就是需要对应环境来运行导出函数。

究极折腾

路径问题

然后 Hugo 默认配置下找不到路径将会直接报错退出,对于 roam 场景下经常附个链接又不打算先写的情况很不友好,搜索下来发现官方文档已经有具体方案了。

Links and Cross References | Hugo

只要在配置文件写入 refLinksErrorLevel = "WARNING" 这行配置就行了,配置成警告而不报错导致中止。

搜索问题

主题自带的搜索无法搜索中文,不过相比 Emacs 相关问题,Hugo 相关真是好找太多了

然后参考了这个方案 Hugo JS Searching with Fuse.js · GitHub ,评论区下面也有人做了中文优化的 Hugo JS Searching with Fuse.js · GitHub ,能抄作业就是爽啦!

评论问题

其实到这里直接 hugo 命令生成静态文件就好了,但趁着折腾劲没过去还得再造会儿~

然后打算为自己这个网站加一个评论区,这次不像我博客那样选用要用 GitHub 的方案了,毕竟 GitHub 对完全没技术背景的人来说挺不友好的,所以打算这次选一个完全不用任何额外条件评论功能。

方案是选择是 Isso ,一个 Python + Sqlite3 的评论实现,总之安装过程就是直接 docker-compose 拉起来就完事了,不过最后嵌入到网站实际使用发现就有麻烦了,因为这个主题选择采用动态加载方式,第二篇文章会加载到侧面,而这里的评论区是不会加载的……鼓捣了半天终于摸索出一个解决方案……

根据官网文档 Advanced integration ,解决方案是加载文档时将原来评论区移除,然后附加到新位置上,然后用

window.Isso.init()
window.Isso.fetchComments()

重新加载评论框…,然后不要忘了在博客模板文件里配置的时候将 section 设置成

<section id="isso-thread" data-title="{{ .Title }}" data-isso-id="{{ .RelPermalink }}">

按路径加载的模式,这样才能确保新评论框加载时没有沿用到旧评论框。

反向链接

模板自带一个反向链接面板,够是够用的,不过 org-roam-v2 新增了酷炫的任意块链接功能怎么能错过呢?所以鼓捣了一下,用 Python bs4 包解析 hugo 生成的 HTML 文件里的内部链接,然后在计算出反链再塞回到原来的文件里…… 25 EDIT: 现在改用纯 elisp 方案啦!

具体代码非常稀烂……都是大循环直接搞定的……要是真想借鉴的话我也放在了我的 Gitea 仓库上…… 25 EDIT: 现在是 forgejo 仓库啦!

结语

总之就是十二分折腾,估计燃尽了我几周的能量吧……

反向链接

评论