(this is a slightly modified extract from my Doom Emacs configuration)
While writing with Org mode, I frequently need to insert links to other headings within my local document. I started by doing this manually, inserting a
CUSTOM_ID property in the destination headline, and then creating the link.
Later, I discovered and now normally use
counsel-org-link (part of counsel, which is included and enabled by default with Ivy in Doom Emacs) for linking between headings in an Org document. It shows me a searchable list of all the headings in the current document, and allows selecting one, automatically creating a link to it. Since it doesn’t have a keybinding by default, let’s start by giving it one (C-c l l is the default
+links section in Doom Emacs):
(map! :after counsel :map org-mode-map
"C-c l l h" #'counsel-org-link)
I also configure
counsel-outline-display-style so that only the headline title is inserted into the link, instead of its full path within the document.
(setq counsel-outline-display-style 'title))
org-id as its backend, which generates IDs using UUIDs and stores them in the
ID property. I prefer using human-readable IDs stored in the
CUSTOM_ID property of each heading, so we need to make some changes.
org-id to use
CUSTOM_ID if it exists. This instructs
org-id to grab those IDs when using the
org-store-link function (funny that
org-id knows how to recognize and use
CUSTOM_ID, but not how to generate them).
;; Do not create ID if a CUSTOM_ID exists
(setq org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id))
Second, I override
counsel-org-link-action, which is the function that actually generates and inserts the link, with a custom function that computes and inserts human-readable
CUSTOM_ID links. This is supported by a few auxiliary functions for generating and storing the
(defun zz/make-id-for-title (title)
"Return an ID based on TITLE."
(let* ((new-id (replace-regexp-in-string "[^[:alnum:]]" "-" (downcase title))))
(defun zz/org-custom-id-create ()
"Create and store CUSTOM_ID for current heading."
(let* ((title (or (nth 4 (org-heading-components)) ""))
(new-id (zz/make-id-for-title title)))
(org-entry-put nil "CUSTOM_ID" new-id)
(org-id-add-location new-id (buffer-file-name (buffer-base-buffer)))
(defun zz/org-custom-id-get-create (&optional where force)
"Get or create CUSTOM_ID for heading at WHERE.
If FORCE is t, always recreate the property."
(let ((old-id (org-entry-get nil "CUSTOM_ID")))
;; If CUSTOM_ID exists and FORCE is false, return it
(if (and (not force) old-id (stringp old-id))
;; otherwise, create it
;; Now override counsel-org-link-action
(defun counsel-org-link-action (x)
"Insert a link to X.
X is expected to be a cons of the form (title . point), as passed
If X does not have a CUSTOM_ID, create it based on the headline
(let* ((id (zz/org-custom-id-get-create (cdr x))))
(org-insert-link nil (concat "#" id) (car x)))))
Ta-da! Now using
counsel-org-link inserts nice, human-readable links.
(tip of the hat: I got a lot of inspiration and some code for this from Emacs Org-mode: Use good header ids!)