CLOSED: [2019-11-16 Sat 17:22] SCHEDULED: <2019-11-16 Sat>
:PROPERTIES:
:CREATED: [2019-11-15 Fri 23:33]
:ID: 2019-11-16-UOMF-Linking-Headings
:END:
:LOGBOOK:
- State "DONE" from "DONE" [2025-06-23 Mon 19:07]
- State "DONE" from "DONE" [2024-11-27 Wed 17:03]
- State "DONE" from "DONE" [2020-07-22 Wed 20:23]
- State "DONE" from "NEXT" [2019-11-16 Sat 17:22]
:END:
- Updates
- 2020-07-22: Added link to my article on auto-backlinks
- 2024-11-27: most current version of =my-generate-sanitized-alnum-dash-string(str)=
- 2025-06-23: Link to the article that explains a rather tedious repair process when not using the workflow described here
This is an article from a series of blog postings. Please do read [[id:2019-09-25-using-orgmode][my
"Using Org Mode Features" (UOMF) series page]] for explanations on
articles of this series.
In this article, I'm going to explain how I am using internal links to
create links pointing to headings using unique =:ID:= properties.
*** Motivation: Why Linking?
One of the most valuable assets I do have in my Org mode is my
personal knowledge base. It consists of a hierarchy like that:
- ideas
- software
- emacs
- python
- git
- vim
- linux
- android
- windows
- macOS
- Firefox
- Chrome/Chromium
- LibreOffice
- Mastodon
- [...]
- hardware
- sport
- standards, laws
- job
- presenting
- research
- education
- languages
- travelling
- food/drinks
- restaurants/bars
- fun
- music
- books
- bookmarks
I only began to outline the sub-headings of "software" so that you get
an idea how my knowledge base looks like. Frequent readers of my blog
already know that I dislike strict hierarchies for many good reasons.
In this case (and [[id:2018-07-22-folder-hierarchy][others]]), I have to stick to a strict hierarchy
because that's was Emacs Org mode is providing me: placing headings
into a hierarchy.
To compensate this limitation, I'm using [[https://orgmode.org/manual/Internal-links.html][internal links]] all the time.
For example, if I add a new heading on a special Firefox plugin that
provides input for my Org mode, I have to place it either in the
"software/emacs/WWW/" section or in "software/Firefox/plugins/". Either
way, I tend to link this heading to the other place as well so that I
might find it using navigation to both logical destinations.
Sometimes, I even have to create more than one link to a heading
because it would correlate with more than two potential locations.
Similar to a [[id:2017-12-24-Zettelkasten][Zettelkasten]].
*** Why Do I Need Unique IDs?
The chapter about [[https://orgmode.org/manual/Handling-Links.html][handling links]] in the Org manual mentions two
methods for working with links:
- =C-c l= runs the command =org-store-link=
- =C-c C-l= inserts the stored (or any previous) link to the buffer
Applied to this heading, this results in following link:
: [[file:public_voit.org::*Why Do I Need Unique IDs?][Why Do I Need Unique IDs?]]
To follow any Org mode link, you can invoke =C-c C-o=
(=org-open-at-point=) or use =RET= or your mouse. I stick to the =C-c
C-o= pattern since it applies to "follow" almost anything Org mode.
As you can see above, this type of internal link is based on the
current Org mode file and the title of the heading. I consider both of
them fragile. This link gets a broken one when I move the heading to a
different file or when I rename the heading.
If you are linking headings with those fragile links, you might notice
that you'll need [[https://spepo.github.io/2025-06-22-fault-tolerant-org-links.html][rather tedious repair mechanism to recover when links
get broken]], which they will do.
Therefore, I prefer methods that are using some kind of unique
identifiers (IDs) for linking. Usually, these IDs are stores as
special properties [[https://orgmode.org/guide/Properties.html][in the =:PROPTERTY:= drawers]].
DISCLAIMER: I have to admit, that for historical reasons *I might use
internal links a bit off-standard*. So please do read about my method
with a grain of salt. Read more about this in the following section.
*** ID Versus CUSTOM_ID Properties
The Org mode manual section about [[https://orgmode.org/manual/Internal-links.html][internal links]] does refer to
=:CUSTOM_ID:= properties for internal links. This provides you the
ability to link to a heading:
: * Example heading
: :PROPERTIES:
: :CUSTOM_ID: This-is-my-unique-ID
: :END:
:
: [[#This-is-my-unique-ID]] is a link to the heading above.
This is, where I'm using a non-standard approach compared to the
manual. I began using internal links when I did not know about
=:CUSTOM_ID:= properties. Back then, I used =:ID:= properties instead.
I once started the [[https://lists.gnu.org/archive/html/emacs-orgmode/2016-12/msg00423.html][discussion about using =:ID:= versus =:CUSTOM_ID:=
properties on the Org mode mailing list]]. My key takeaways were:
- As long as the IDs are unique, it should not matter what is used.
- [[https://lists.gnu.org/archive/html/emacs-orgmode/2016-12/msg00481.html][There are methods to adapt the usual ID creation method]].
- [[https://lists.gnu.org/archive/html/emacs-orgmode/2016-12/msg00482.html][Carsten is using =:CUSTOM_ID:= properties "to make HTML targets stable and meaningful"]].
So I kept using =:ID:=.
One thing you have to make sure when you also want to use =:ID:=
properties for linking: they need to be "activated" as described [[https://orgmode.org/manual/Handling-Links.html][in
the manual]] with =(require 'org-id)= or similar.
*** Creating Unique ID Properties
Not only [[id:2019-10-26-all-things-org][in the "All Things Org Mode" video with John Wiegley]] you can
see that [[https://en.wikipedia.org/wiki/Uuid][UUIDs]] could be generated by external tools like [[http://man7.org/linux/man-pages/man1/uuidgen.1.html][uuidgen]] (see
=org-inline-note()= on [[https://github.com/jwiegley/dot-emacs/blob/master/dot-org.el][John's Org mode setup]]). This results in UUIDs
like =dd4b6da4-aabc-4c38-bdfa-e901c110d698= which obviously does
fulfill the most important requirement of UUIDs: uniqueness.
However, I personally do find it helpful when I do get an idea about
the link target within the link itself. I seldom use descriptions for
internal links because I'm lazy. This is why I was creating my IDs
manually until a couple of days ago. I switched to the =:PROPERTIES:=
drawer, added a new line, entered =:ID:= typically followed by a
date-stamp (easily added by a keyboard command) and a short
description of the heading. For this heading, this would have looked
like that:
: *** Creating Unique ID Properties
: :PROPERTIES:
: :ID: 2019-11-16-id-properties
: :END:
The uniqueness was introduces by pre-pending with the date-stamp:
during one single day I remember that I might have created a similar
heading already. It's unlikely that I may create identical IDs this
way.
When a workflow like this becomes a habit, I tend not to think about
it for the longest time.
Sadly.
Until recently, when I got the idea of making my life a bit easier. I
overcame my aversion to elisp and came up with
=my-id-get-or-generate()= and its supporting function
=my-generate-sanitized-alnum-dash-string(str)=:
#+BEGIN_SRC emacs-lisp
(defun my-generate-sanitized-alnum-dash-string(str)
"Returns a string which contains only a-zA-Z0-9 with single dashes
replacing all other characters in-between them.
Some parts were copied and adapted from org-hugo-slug
from https://github.com/kaushalmodi/ox-hugo (GPLv3)."
(let* (;; Remove ".." HTML tags if present.
(case-fold-search nil)
(str (replace-regexp-in-string "<\\(?1:[a-z]+\\)[^>]*>.*\\1>" "" str))
;; Remove URLs if present in the string. The ")" in the
;; below regexp is the closing parenthesis of a Markdown
;; link: [Desc](Link).
(str (replace-regexp-in-string (concat "\\](" ffap-url-regexp "[^)]+)") "]" str))
;; Replace "&" with " and ", "." with " dot ", "+" with
;; " plus ".
(str (replace-regexp-in-string
"&" " and "
(replace-regexp-in-string
"\\." " dot "
(replace-regexp-in-string
"\\+" " plus " str))))
;; Replace German Umlauts with 7-bit ASCII.
(str (replace-regexp-in-string "ä" "ae" str nil))
(str (replace-regexp-in-string "ü" "ue" str nil))
(str (replace-regexp-in-string "ö" "oe" str nil))
(str (replace-regexp-in-string "ß" "ss" str nil))
;; Replace all characters except alphabets, numbers and
;; parentheses with spaces.
(str (replace-regexp-in-string "[^[:alnum:]()]" " " str))
;; On emacs 24.5, multibyte punctuation characters like ":"
;; are considered as alphanumeric characters! Below evals to
;; non-nil on emacs 24.5:
;; (string-match-p "[[:alnum:]]+" ":")
;; So replace them with space manually..
(str (if (version< emacs-version "25.0")
(let ((multibyte-punctuations-str ":")) ;String of multibyte punctuation chars
(replace-regexp-in-string (format "[%s]" multibyte-punctuations-str) " " str))
str))
;; Remove leading and trailing whitespace.
(str (replace-regexp-in-string "\\(^[[:space:]]*\\|[[:space:]]*$\\)" "" str))
;; Replace 2 or more spaces with a single space.
(str (replace-regexp-in-string "[[:space:]]\\{2,\\}" " " str))
;; Replace parentheses with double-hyphens.
(str (replace-regexp-in-string "\\s-*([[:space:]]*\\([^)]+?\\)[[:space:]]*)\\s-*" " -\\1- " str))
;; Remove any remaining parentheses character.
(str (replace-regexp-in-string "[()]" "" str))
;; Replace spaces with hyphens.
(str (replace-regexp-in-string " " "-" str))
;; Remove leading and trailing hyphens.
(str (replace-regexp-in-string "\\(^[-]*\\|[-]*$\\)" "" str)))
str)
)
#+END_SRC
=org-hugo-slug= from [[https://github.com/kaushalmodi/ox-hugo][ox-hugo]] was very similar. I removed for example
the lowercase-part and added the umlaut-part which still contains a
minor uppercase-bug. Drop me a line if you can fix this.
And here comes the main function:
#+BEGIN_SRC emacs-lisp
(defun my-id-get-or-generate()
"Returns the ID property if set or generates and returns a new one if not set.
The generated ID is stripped off potential progress indicator cookies and
sanitized to get a slug. Furthermore, it is prepended with an ISO date-stamp
if none was found before."
(interactive)
(when (not (org-id-get))
(progn
(let* (
;; retrieve heading string
(my-heading-text (nth 4 (org-heading-components)))
;; remove progress indicators like "[2/7]" or "[25%]"
(my-heading-text (replace-regexp-in-string "[[][0-9%/]+[]] " "" my-heading-text))
;; get slug from heading text
(new-id (my-generate-sanitized-alnum-dash-string my-heading-text))
)
(when (not (string-match "[12][0-9][0-9][0-9]-[01][0-9]-[0123][0-9]-.+" new-id))
;; only if no ISO date-stamp is found at the beginning of the new id:
(setq new-id (concat (format-time-string "%Y-%m-%d-") new-id)))
(org-set-property "ID" new-id)
)
)
)
(kill-new (org-id-get));; put ID in kill-ring
(org-id-get);; retrieve the current ID in any case as return value
)
;(bind-key (kbd "I") #'my-id-get-or-generate my-map) ;; -> my binding
#+END_SRC
You can find the latest versions of the functions [[https://github.com/novoid/dot-emacs/blob/master/config.org][in my Emacs
configuration]].
I mapped it to =my-map I= which results in =C-c C-, I= which is
relatively easy to type. This results in human-readable, unique =:ID:=
properties like =2019-11-16-Creating-Unique-ID-Properties= for this
heading.
Here is a screencast showing the function in action:
#+BEGIN_EXPORT html
#+END_EXPORT
You can see, that my link workflow now looks like this:
1. Go to target heading.
- It doesn't have to be the heading line.
2. Get the ID by invoking =my-id-get-or-generate()=.
- If the heading did not have an =:ID:= property, it gets generated.
3. Go to the spot where the link to the target should be created.
4. Type =id:= followed by the ID via pasting from the kill-ring.
While generating the whole =id:= link including the ID could be easily
automated, I prefer it this way. In many cases, I just need the raw ID
instead of the internal link ([[https://orgmode.org/worg/org-contrib/org-depend.html][org-depend]]).
*** Ideas for the Future
While this being a snappy workflow for now, I can think of further
functionality here. For example, how about fully bi-directional ID
links between source and target? This way, I would know exactly what is
linking to my current heading. Those back-links could be automatically
stored in special properties, for example.
What is also on my mind currently is a neat workflow for easy to use
task planning using either [[https://orgmode.org/worg/org-contrib/org-depend.html][org-depend]] or [[https://github.com/akirak/org-edna/][org-edna]]. Linking [[id:2019-11-03-org-projects][project
tasks]] according their dependencies is labor-intense and error-prone.
The ideas are there, the concept almost finished but my inability of
finding the proper elisp functions to implement it is a hindrance. If
you would like to volunteer, drop me a line. I'm convinced that this
won't be much effort/lines-of-code and it will provide an easy to use
workflow for planning even advanced projects.
*Update: Please do read [[id:2020-07-11-org-super-links][this article about org-super-links]]* which
provides back-links so that you get bi-directional links between
headings. Furthermore, [[https://github.com/toshism/org-super-links/issues/21][this feature request]] is a proposal for my idea
on improving dependencies between headings.