Export backlinks on Org export

Thanks! Didn’t even think about using org headings for that.

1 Like

I have try your snippet code to export org files, but it still has rough edges:

An org file named Foo:

#+title: Foo

It contains [[id:bar][Bar]]

Another org file named Bar:

#+title: Bar

* Outline 1
xxxxxxxxxxx

* Outline 2
yyyyyyyyyy[fn:1]

* Footnotes

[fn:1] This is footnote 1.

Then open file Foo, and call interactive command org-publish-current-file, then the publish process will be interrupted by error:

Definition not found for footnote 1

I presume the command org-roam-node-file or org-roam-node-preview in your snippet can not find the footnote 1, so the emacs throw an error.

I am not so sure, because I am not so familiar with org-roam source code, don’t konw how to fix it.
Maybe there are some remedies to this problem?

1 Like

After some effort, I remedy this rough edge.

(defun hurricane//collect-backlinks-string (backend)
  (when (org-roam-node-at-point)
    (let* ((source-node (org-roam-node-at-point))
           (source-file (org-roam-node-file source-node))
           (nodes-in-file (--filter (s-equals? (org-roam-node-file it) source-file)
                                    (org-roam-node-list)))
           (nodes-start-position (-map 'org-roam-node-point nodes-in-file))
           ;; Nodes don't store the last position, so get the next headline position
           ;; and subtract one character (or, if no next headline, get point-max)
           (nodes-end-position (-map (lambda (nodes-start-position)
                                       (goto-char nodes-start-position)
                                       (if (org-before-first-heading-p) ;; file node
                                           (point-max)
                                         (call-interactively
                                          'org-forward-heading-same-level)
                                         (if (> (point) nodes-start-position)
                                             (- (point) 1) ;; successfully found next
                                           (point-max)))) ;; there was no next
                                     nodes-start-position))
           ;; sort in order of decreasing end position
           (nodes-in-file-sorted (->> (-zip nodes-in-file nodes-end-position)
                                      (--sort (> (cdr it) (cdr other))))))
      (dolist (node-and-end nodes-in-file-sorted)
        (-when-let* (((node . end-position) node-and-end)
                     (backlinks (--filter (->> (org-roam-backlink-source-node it)
                                               (org-roam-node-file)
                                               (s-contains? "private/") (not))
                                          (org-roam-backlinks-get node)))
                     (heading (format "\n\n%s Links to this node\n"
                                      (s-repeat (+ (org-roam-node-level node) 1) "*")))
                     (properties-drawer ":PROPERTIES:\n:HTML_CONTAINER_CLASS: references\n:END:\n"))
          (goto-char end-position)
          (insert heading)
          (insert properties-drawer)
          (dolist (backlink backlinks)
            (let* ((source-node (org-roam-backlink-source-node backlink))
                   (source-file (org-roam-node-file source-node))
                   (properties (org-roam-backlink-properties backlink))
                   (outline (when-let ((outline (plist-get properties :outline)))
                              (when (> (length outline) 1)
                                (mapconcat #'org-link-display-format outline " > "))))
                   (point (org-roam-backlink-point backlink))
                   (text (s-replace "\n" " " (org-roam-preview-get-contents
                                              source-file
                                              point)))
                   (reference (format "%s [[id:%s][%s]]\n%s\n%s\n\n"
                                      (s-repeat (+ (org-roam-node-level node) 2) "*")
                                      (org-roam-node-id source-node)
                                      (org-roam-node-title source-node)
                                      (if outline (format "%s (/%s/)"
                                                          (s-repeat (+ (org-roam-node-level node) 3) "*") outline) "")
                                      text))
                   (label-list (with-temp-buffer
                                 (insert text)
                                 (org-element-map (org-element-parse-buffer) 'footnote-reference
                                   (lambda (reference)
                                     (org-element-property :label reference)))))
                   (footnote-string-list
                      (with-temp-buffer
                        (insert-file-contents source-file)
                        (-map (lambda (label) (buffer-substring-no-properties
                                               (nth 1 (org-footnote-get-definition label))
                                               (nth 2 (org-footnote-get-definition label))))
                              label-list))))
              (-map (lambda (footnote-string) (insert footnote-string)) footnote-string-list)
              (insert reference))))))))

(add-hook 'org-export-before-processing-hook 'hurricane//collect-backlinks-string)
1 Like

How would I modify this code so that ONLY the private tag is linked and exported. My lisp is so poor. Thanks!

 (dolist (node-and-end nodes-in-file-sorted)
        (-when-let* (((node . end-position) node-and-end)
                     (backlinks (--filter (->> (org-roam-backlink-source-node it)
                                               (org-roam-node-file)
                                               (s-contains? "private/") (not))
                                          (org-roam-backlinks-get node)))
                     (heading (format "\n\n%s Links to this node\n"
                                      (s-repeat (+ (org-roam-node-level node) 1) "*")))
                     (properties-drawer ":PROPERTIES:\n:HTML_CONTAINER_CLASS: references\n:END:\n"))

If node has aliases, there will be as many “links to this node” as aliases are.

It seems like most of the methods here won’t work on --batch mode. See this other topic.
It might be caused by the interactive calls. Does anyone here have any idea on how to fix this?

Are you using V2 of Org-roam? The other post has this:

(org-roam-sql [:select [file-from] :from file-links :where (= file-to $s1)] file))

If you are on V2, org-roam-sql does not exist, and the table named file-links does not, either. Refer to this section of the manual, where this example is mentioned:

(org-roam-db-query [:select * :from nodes])

For correct table and field names, see C-h v of variable org-roam-db--table-schemata. I tink you are going for table links for field dest.

I’m able to run the functions above if I’m already inside Emacs. However I’m unable to generate the backlinks when I try to run the script in --batch mode.

I’m trying to run the following code with the command emacs -q --batch -l publish.el and it does publish my website but it doesn’t generate the backlinks below each note.

(require 'ox-publish)
(require 'org-roam)

(setq org-roam-directory "."
      org-id-locations-file ".orgids"
      org-id-link-to-org-use-id t
      org-id-extra-files (org-roam-list-files)
      org-id-track-globally t
      org-export-with-broken-links 'mark

      ;; HTML
      org-html-validation-link nil
      org-html-home/up-format "<!-- %s --><nav><a href=\"%s\">Index</a></nav>"
      org-html-link-home "index.html"
      org-html-head-include-scripts nil
      org-html-head-include-default-style nil
      org-html-head  "<link rel=\"stylesheet\" href=\"assets/bamboo.css\">"

      publish-directory "html")

(defun collect-backlinks-string (backend)
  (when (org-roam-node-at-point)
    (let* ((source-node (org-roam-node-at-point))
           (source-file (org-roam-node-file source-node))
           (nodes-in-file (--filter (s-equals? (org-roam-node-file it) source-file)
                                    (org-roam-node-list)))
           (nodes-start-position (-map 'org-roam-node-point nodes-in-file))
           ;; Nodes don't store the last position, so get the next headline position
           ;; and subtract one character (or, if no next headline, get point-max)
           (nodes-end-position (-map (lambda (nodes-start-position)
                                       (goto-char nodes-start-position)
                                       (if (org-before-first-heading-p) ;; file node
                                           (point-max)
                                          ('org-forward-heading-same-level)
                                         (if (> (point) nodes-start-position)
                                             (- (point) 1) ;; successfully found next
                                           (point-max)))) ;; there was no next
                                     nodes-start-position))
           ;; sort in order of decreasing end position
           (nodes-in-file-sorted (->> (-zip nodes-in-file nodes-end-position)
                                      (--sort (> (cdr it) (cdr other))))))
      (dolist (node-and-end nodes-in-file-sorted)
        (-when-let* (((node . end-position) node-and-end)
                     (backlinks (--filter (->> (org-roam-backlink-source-node it)
                                               (org-roam-node-file)
                                               (s-contains? "private/") (not))
                                          (org-roam-backlinks-get node)))
                     (heading (format "\n\n%s Backlinks\n"
                                      (s-repeat (+ (org-roam-node-level node) 1) "*")))
                     (properties-drawer ":PROPERTIES:\n:HTML_CONTAINER_CLASS: references\n:END:\n"))
          (goto-char end-position)
          (insert heading)
          (insert properties-drawer)
          (dolist (backlink backlinks)
            (let* ((source-node (org-roam-backlink-source-node backlink))
                   (source-file (org-roam-node-file source-node))
                   (properties (org-roam-backlink-properties backlink))
                   (outline (when-let ((outline (plist-get properties :outline)))
                              (when (> (length outline) 1)
                                (mapconcat #'org-link-display-format outline " > "))))
                   (point (org-roam-backlink-point backlink))
                   (text  (org-roam-preview-get-contents
                           source-file
                           point))
                   (reference (format "\n ----- \n%s [[id:%s][%s]]\n%s\n%s\n\n"
                                      (s-repeat (+ (org-roam-node-level node) 2) "*")
                                      (org-roam-node-id source-node)
                                      (org-roam-node-title source-node)
                                      (if outline (format "%s (/%s/)"
                                                          (s-repeat (+ (org-roam-node-level node) 3) "*") outline) "")
                                      text))
                   (label-list (with-temp-buffer
                                 (insert text)
                                 (org-element-map (org-element-parse-buffer) 'footnote-reference
                                   (lambda (reference)
                                     (org-element-property :label reference)))))
                   (footnote-string-list
                    (with-temp-buffer
                      (insert-file-contents source-file)
                      (-map (lambda (label) (buffer-substring-no-properties
                                             (nth 1 (org-footnote-get-definition label))
                                             (nth 2 (org-footnote-get-definition label))))
                            label-list))))
              (-map (lambda (footnote-string) (insert footnote-string)) footnote-string-list)
              (insert reference))))))))

(add-hook 'org-export-before-processing-hook 'collect-backlinks-string)

(setq org-publish-project-alist
      (list
       (list "org-site:main"
             :recursive nil
             :exclude ".*gitignore|.*/private/.*" ;; remove private directory
             :base-directory org-roam-directory
             :publishing-function 'org-html-publish-to-html
             :publishing-directory publish-directory
             :with-date t
             :with-author nil           ;; Don't include author name
             :with-creator t            ;; Include Emacs and Org versions in footer
             :with-toc t                ;; Include a table of contents
             :section-numbers nil       ;; Don't include section numbers
             :time-stamp-file nil
             :auto-sitemap t
             :sitemap-filename "index.org"
             :sitemap-sort-files 'alphabetically
             :sitemap-ignore-case t
             :sitemap-title "personal digital garden")
       (list "org-site:static"
             :base-directory org-roam-directory
             :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf"
             :recursive t
             :publishing-directory publish-directory
             :publishing-function 'org-publish-attachment)))

(org-publish-all t)

The —batch option implies -q (and you explicitly adds it in the command line). I’m guessing you would need to explicitly add load-path for org-roam before you require it.

I do have org-roam on my load-path. The problem is that the function needs something interactively so I can’t run it on --batch I guess.

Thanks for the fast replies, though!

You may well be right about interactive but org-publish is also an interactive function isn’t it? (but you can run it in the batch mode).

I don’t see load-path in your command in terminal or in the script… init.el (or .emacs) won’t be called — I guess you know that.

I also had this issue. Here’s how I fixed it:
TLDR: Check if org-roam-db-location is correct.

I figured out the variables nodes-in-file-sorted was nil so (do list never entered. From there, I discovered (org-roam-at-point) was returning little information: #s(org-roam-node nil nil nil nil nil 6a6d5036-8acd-46b3-4550-9e737271d8c0 nil 1 nil nil nil nil nil nil nil nil nil nil) The function needs more data to work.
If you see the documentation of (org-roam-at-point),

This function also returns the node if it has yet to be cached in the
database. In this scenario, only expect :id and :point to be
populated.

This is exactly what was happening. I discovered org-roam-db-location was pointing to the wrong location, because batch mode doesn’t come with my usual configuration.