Export backlinks on Org export

I’m trying to figure out how to insert a properties drawer on the backlinks node so I can style it with CSS.

when I try:

        (insert heading)
        (insert ":PROPERTIES:\n:HTML_CONTAINER_CLASS: references\n:END:\n")

it does not insert the text correctly, instead it inserts it as formatted text within the node. Any suggestions?

You also need to increase the character count, make it like this bellow the (insert heading):

;; Insert properties drawer
(setq properties-drawer ":PROPERTIES:\n:HTML_CONTAINER_CLASS: references\n:END:\n")
;; Count the characters and count the new lines (3)
(setq character-count (+ 3 character-count (string-width properties-drawer)))
(insert properties-drawer)

Hm, this is resulting in the same output.

This is what I used. I made 2 changes:

  • Removed one \n from the heading
  • Changed (setq character-count (+ 4 to (setq character-count (+ 3 because now it only has 3 new lines.
(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))
           ;; Sort the nodes by the point to avoid errors when inserting the
           ;; references
           (nodes-in-file (--sort (< (org-roam-node-point it)
                                     (org-roam-node-point other))
                                  (-filter (lambda (node)
                                             (s-equals?
                                              (org-roam-node-file node)
                                              source-file))
                                           (org-roam-node-list))))
           ;; Nodes don't store the last position so, get the next node position
           ;; and subtract one character
           (nodes-start-position (-map (lambda (node) (org-roam-node-point node))
                                       nodes-in-file))
           (nodes-end-position (-concat (-map (lambda (next-node-position)
                                                (- next-node-position 1))
                                              (-drop 1 nodes-start-position))
                                        (list (point-max))))
           ;; Keep track of the current-node index
           (current-node 0)
           ;; Keep track of the amount of text added
           (character-count 0))
      (dolist (node nodes-in-file)
        (when (org-roam-backlinks-get node)
          ;; Go to the end of the node and don't forget about previously inserted
          ;; text
          (goto-char (+ (nth current-node nodes-end-position) character-count))
          ;; Add the references as a subtree of the node
          (setq heading (format "\n\n%s References\n"
                                (s-repeat (+ (org-roam-node-level node) 1) "*")))
          ;; Count the characters and count the new lines (4)
          (setq character-count (+ 3 character-count (string-width heading)))
          (insert heading)
          ;; Insert properties drawer
          (setq properties-drawer ":PROPERTIES:\n:HTML_CONTAINER_CLASS: references\n:END:\n")
          ;; Count the characters and count the new lines (3)
          (setq character-count (+ 3 character-count (string-width properties-drawer)))
          (insert properties-drawer)
          (dolist (backlink (org-roam-backlinks-get node))
            (let* ((source-node (org-roam-backlink-source-node backlink))
                   (point (org-roam-backlink-point backlink))
                   (text (org-roam-get-preview
                          (org-roam-node-file source-node)
                          point))
                   (references (format "- [[./%s][%s]]: %s\n\n"
                                       (file-relative-name (org-roam-node-file source-node))
                                       (org-roam-node-title source-node)
                                       text)))
              ;; Also count the new lines (2)
              (setq character-count (+ 2 character-count (string-width references)))
              (insert references))))
        (setq current-node (+ current-node 1))))))

Thank you so much! It works.

1 Like

Since today I receive this error:

Symbol's function definition is void: org-roam-get-preview

Removing the call to org-roam-get-preview from collect-backlinks-string gets things working again.

Not sure what happened to org-roam-get-preview - maybe related to this PR? - (feat): globally restructure and refactor the codebase by Wetlize · Pull Request #1724 · org-roam/org-roam · GitHub - which does seem to rework a lot of the code.

The function changed. Now it’s called: org-roam-preview-get-contents

I’ve changed the code accordingly and removed new lines from preview, to avoid creating new paragraphs.

(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))
           ;; Sort the nodes by the point to avoid errors when inserting the
           ;; references
           (nodes-in-file (--sort (< (org-roam-node-point it)
                                     (org-roam-node-point other))
                                  (-filter (lambda (node)
                                             (s-equals?
                                              (org-roam-node-file node)
                                              source-file))
                                           (org-roam-node-list))))
           ;; Nodes don't store the last position so, get the next node position
           ;; and subtract one character
           (nodes-start-position (-map (lambda (node) (org-roam-node-point node))
                                       nodes-in-file))
           (nodes-end-position (-concat (-map (lambda (next-node-position)
                                                (- next-node-position 1))
                                              (-drop 1 nodes-start-position))
                                        (list (point-max))))
           ;; Keep track of the current-node index
           (current-node 0)
           ;; Keep track of the amount of text added
           (character-count 0))
      (dolist (node nodes-in-file)
        (when (org-roam-backlinks-get node)
          ;; Go to the end of the node and don't forget about previously inserted
          ;; text
          (goto-char (+ (nth current-node nodes-end-position) character-count))
          ;; Add the references as a subtree of the node
          (setq heading (format "\n\n%s References\n"
                                (s-repeat (+ (org-roam-node-level node) 1) "*")))
          ;; Count the characters and count the new lines (4)
          (setq character-count (+ 3 character-count (string-width heading)))
          (insert heading)
          ;; Insert properties drawer
          (setq properties-drawer ":PROPERTIES:\n:HTML_CONTAINER_CLASS: references\n:END:\n")
          ;; Count the characters and count the new lines (3)
          (setq character-count (+ 3 character-count (string-width properties-drawer)))
          (insert properties-drawer)
          (dolist (backlink (org-roam-backlinks-get node))
            (let* ((source-node (org-roam-backlink-source-node backlink))
                   (point (org-roam-backlink-point backlink))
                   (text (s-replace "\n" " " (org-roam-preview-get-contents
                                           (org-roam-node-file source-node)
                                           point)))
                   (references (format "- [[./%s][%s]]: %s\n\n"
                                       (file-relative-name (org-roam-node-file source-node))
                                       (org-roam-node-title source-node)
                                       text)))
              ;; Also count the new lines (2)
              (setq character-count (+ 2 character-count (string-width references)))
              (insert references))))
        (setq current-node (+ current-node 1))))))
3 Likes

Hey, this function is nice, thanks! I’m using it on my website.

I’ve fixed a few bugs in the way it works below. Namely, a node’s end position isn’t necessarily the next node, but rather the next same-level headline (if there is one.) Also, I changed it to sort in decreasing order of the node’s end position (which removes the need for manually counting characters entirely,) and then insert backlinks in that order.

A couple other tweaks I added:

  • the outline path, as is displayed in the org-roam-buffer. You can take this out if desired.
  • backlinks link directly to the node ID, rather than to the source file.
(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)
                                         (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)
        (-let (((node . end-position) node-and-end))
          (when (org-roam-backlinks-get node)
            (goto-char end-position)
            ;; Add the references as a subtree of the node
            (setq heading (format "\n\n%s References\n"
                                  (s-repeat (+ (org-roam-node-level node) 1) "*")))
            (insert heading)
            (setq properties-drawer ":PROPERTIES:\n:HTML_CONTAINER_CLASS: references\n:END:\n")
            (insert properties-drawer)
            (dolist (backlink (org-roam-backlinks-get node))
              (let* ((source-node (org-roam-backlink-source-node backlink))
                     (properties (org-roam-backlink-properties backlink))
                     (outline (when-let ((outline (plist-get properties :outline)))
                                  (mapconcat #'org-link-display-format outline " > ")))
                     (point (org-roam-backlink-point backlink))
                     (text (s-replace "\n" " " (org-roam-preview-get-contents
                                                (org-roam-node-file source-node)
                                                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)))
                (insert reference)))))))))
2 Likes

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.