1. The issue

I use org-roam to connect my notes/documents and it uses id to link them.

When I decide I want to publish a file to my blog, I use org's default publishing commands. I have some custom setup for it in my emacs dot files. However because some of the internal links link to files that are not published, for example \[[xxfj-el....][org mode]\], I always get \[BROKEN LINK xxxxxx\] in my exported posts like org mode plot data and save to file below given that I have set org-export-with-broken-links to 'mark.

20231008-131732_screenshot.jpeg

This is not very nice to say the least. And this is actually the best you can get with the three options for org-export-with-broken-links, which are nil, non-nil and mark.

With nil, a broken link fails the export. non-nil simply ignores the link. And mark produces the above screenshot.

However, I really want this to be able to ignore the org id and just show the text I put in after the id, which in the above should be org mode.

2. The solution

I did a bit digging and came up with this solution. It works pretty well. All it does is adding a new option to org-export-with-broken-links called label and provide its content in org-export-resolve-id-link.

See the commit for a diff view of the changes I did in the following functions.

(defun org-export-resolve-id-link (link info)
  "Return headline referenced as LINK destination.

INFO is a plist used as a communication channel.

Return value can be the headline element matched in current parse
tree or a file name.  Assume LINK type is either \"id\" or
\"custom-id\".  Throw an error if no match is found."
  (let ((id (org-element-property :path link))
        (contents (car (last link))))
    ;; First check if id is within the current parse tree.
    (or (org-element-map (plist-get info :parse-tree) 'headline
          (lambda (headline)
            (when (or (equal (org-element-property :ID headline) id)
                      (equal (org-element-property :CUSTOM_ID headline) id))
              headline))
          info 'first-match)
        ;; Otherwise, look for external files.
        (cdr (assoc id (plist-get info :id-alist)))
        (signal 'org-link-broken (list contents)))))

(defun org-export-resolve-id-link (link info)
  "Return headline referenced as LINK destination.

INFO is a plist used as a communication channel.

Return value can be the headline element matched in current parse
tree or a file name.  Assume LINK type is either \"id\" or
\"custom-id\".  Throw an error if no match is found."
  (let ((id (org-element-property :path link))
        (contents (car (last
                        link
                        )))
        )
    ;; First check if id is within the current parse tree.
    (or (org-element-map (plist-get info :parse-tree) 'headline
          (lambda (headline)
            (when (or (equal (org-element-property :ID headline) id)
                      (equal (org-element-property :CUSTOM_ID headline) id))
              headline))
          info 'first-match)
        ;; Otherwise, look for external files.
        (cdr (assoc id (plist-get info :id-alist)))
        (signal 'org-link-broken (list contents)))))

(defun org-export-data (data info)
  "Convert DATA into current back-end format.

DATA is a parse tree, an element or an object or a secondary
string.  INFO is a plist holding export options.

Return a string."
  (or (gethash data (plist-get info :exported-data))
      ;; Handle broken links according to
      ;; `org-export-with-broken-links'.
      (cl-macrolet
          ((broken-link-handler
             (&rest body)
             `(condition-case err
                  (progn ,@body)
                (org-link-broken
                 (pcase (plist-get info :with-broken-links)
                   (`nil (user-error "Unable to resolve link: %S" (nth 1 err)))
                   (`mark (org-export-data
                           (format "[BROKEN LINK: %s]" (nth 1 err)) info))
                   (`label (org-export-data
                            (progn
                              (format "%s" (nth 1 err))) info))
                   (_ nil))))))
        (let* ((type (org-element-type data))
               (parent (org-export-get-parent data))
               (results
                (cond
                 ;; Ignored element/object.
                 ((memq data (plist-get info :ignore-list)) nil)
                 ;; Raw code.
                 ((eq type 'raw) (car (org-element-contents data)))
                 ;; Plain text.
                 ((eq type 'plain-text)
                  (org-export-filter-apply-functions
                   (plist-get info :filter-plain-text)
                   (let ((transcoder (org-export-transcoder data info)))
                     (if transcoder (funcall transcoder data info) data))
                   info))
                 ;; Secondary string.
                 ((not type)
                  (mapconcat (lambda (obj) (org-export-data obj info)) data ""))
                 ;; Element/Object without contents or, as a special
                 ;; case, headline with archive tag and archived trees
                 ;; restricted to title only.
                 ((or (not (org-element-contents data))
                      (and (eq type 'headline)
                           (eq (plist-get info :with-archived-trees) 'headline)
                           (org-element-property :archivedp data)))
                  (let ((transcoder (org-export-transcoder data info)))
                    (or (and (functionp transcoder)
                             (broken-link-handler
                              (funcall transcoder data nil info)))
                        ;; Export snippets never return a nil value so
                        ;; that white spaces following them are never
                        ;; ignored.
                        (and (eq type 'export-snippet) ""))))
                 ;; Element/Object with contents.
                 (t
                  (let ((transcoder (org-export-transcoder data info)))
                    (when transcoder
                      (let* ((greaterp (memq type org-element-greater-elements))
                             (objectp
                              (and (not greaterp)
                                   (memq type org-element-recursive-objects)))
                             (contents
                              (mapconcat
                               (lambda (element) (org-export-data element info))
                               (org-element-contents
                                (if (or greaterp objectp) data
                                  ;; Elements directly containing
                                  ;; objects must have their indentation
                                  ;; normalized first.
                                  (org-element-normalize-contents
                                   data
                                   ;; When normalizing first paragraph
                                   ;; of an item or
                                   ;; a footnote-definition, ignore
                                   ;; first line's indentation.
                                   (and
                                    (eq type 'paragraph)
                                    (memq (org-element-type parent)
                                          '(footnote-definition item))
                                    (eq (car (org-element-contents parent))
                                        data)
                                    (eq (org-element-property :pre-blank parent)
                                        0)))))
                               "")))
                        (broken-link-handler
                         (funcall transcoder data
                                  (if (not greaterp) contents
                                    (org-element-normalize-string contents))
                                  info)))))))))
          ;; Final result will be memoized before being returned.
          (puthash
           data
           (cond
            ((not results) "")
            ((memq type '(nil org-data plain-text raw)) results)
            ;; Append the same white space between elements or objects
            ;; as in the original buffer, and call appropriate filters.
            (t
             (org-export-filter-apply-functions
              (plist-get info (intern (format ":filter-%s" type)))
              (let ((blank (or (org-element-property :post-blank data) 0)))
                (if (eq (org-element-class data parent) 'object)
                    (concat results (make-string blank ?\s))
                  (concat (org-element-normalize-string results)
                          (make-string blank ?\n))))
              info)))
           (plist-get info :exported-data))))))