By default when org-mode documents are exported to HTML, the section and sub-section links look like


The problem is that the above link won't point to the correct section if I rearrange the sections.

How can we have permalinks for each section auto-generated that look like below?


1 Answer 1


You can get this result by setting the CUSTOM_ID property.

From the following file:

* Test
:CREATED:  [2014-10-02 Thu 11:48]
** Sub no custom
:CREATED:  [2014-10-02 Thu 11:49]
** Sub custom
:CREATED:  [2014-10-02 Thu 11:49]
:CUSTOM_ID: Custom

I get the following Export (C-c C-e h H):

<div id="text-table-of-contents">
<li><a href="#sec-1">1. Test</a>
<li><a href="#sec-1-1">1.1. Sub no custom</a></li>
<li><a href="#Custom">1.2. Sub custom</a></li>
<div id="outline-container-sec-1" class="outline-2">
<h2 id="sec-1"><span class="section-number-2">1</span> Test</h2>
<div class="outline-text-2" id="text-1">
<div id="outline-container-sec-1-1" class="outline-3">
<h3 id="sec-1-1"><span class="section-number-3">1.1</span> Sub no custom</h3>
<div class="outline-text-3" id="text-1-1">
<div id="outline-container-Custom" class="outline-3">
<h3 id="Custom"><a id="sec-1-2"></a><span class="section-number-3">1.2</span> Sub custom</h3>

So Sub no custom is linked to by #sec-1-1 while Sub custom uses #custom as the reference.

AutoGenerating IDs

Adding org-id to the list of loaded org-modules or directly evaluating (require 'org-id) will allow for IDs to be generated using org-id-get-create. The following will insert IDs automatically and use them on export.

;; Use custom ID if present, otherwise create a new one when trying to
;; resolve links
(setq org-id-link-to-org-use-id

;; Based on org-expiry-insinuate
(add-hook 'org-insert-heading-hook 'org-id-get-create)
(add-hook 'org-after-todo-state-change-hook 'org-id-get-create)
(add-hook 'org-after-tags-change-hook 'org-id-get-create)

This should look through your existing headlines in a buffer and update the IDs. It will only create IDs if none are present.

(defun my/org-update-ids ()
  (let* ((tree (org-element-parse-buffer 'headline))
         (map (reverse
               (org-element-map tree 'headline
                 (lambda (hl)
                   (org-element-property :begin hl))))))
      (cl-loop for point in map do
               (goto-char point)

Note. This will not fix TOC links to point to the correct IDs. TOC is only configured to use CUSTOM_ID or sec-#-# for generating links. You can however access the sections by their ID's (I would suggest changing org-id-method to org from uuid to shorten the ID length if you intend to use it this way.

CUSTOM_ID is still probably your best bet if you actually want human-legible IDs for the headlines. my/org-update-ids should be able to be used as a starting point for that (map through the buffer for each headline, go to headline then perform action).

A starting point would be (org-entry-put (point) "CUSTOM_ID" id) for setting and (org-entry-get (point) "CUSTOM_ID") for getting. Determinining what to use as id is dependant on how you want them named.

  • Thanks! I will cook up something to auto generate the :CUSTOM_ID: values and post here. This is my plan: It would be cumbersome to type the :PROPERTIES: block for each sub-section. Instead an elisp code can generate those for me and auto generate the :CUSTOM_ID: if one doesn't exist; all in the org-export-before-processing-hook so that the source org file is not modified. It might take me some time to implement this, but will post it here when done. Oct 2, 2014 at 15:56
  • I've provided a method to create :ID: properties for each (and to update your buffer to include them where missing). If you create a function to dynamically assign IDs to headlines (based on headline name?) you can substitute it in for org-id-get-create and it will use create the :Custom_ID: instead. Oct 2, 2014 at 16:54
  • my/org-update-ids only affects a single element. How to update all IDs at once?
    – akater
    Jul 10, 2019 at 17:10
  • 1
    Updated to fix that issue. I'd tested it again and realized that it was only updating certain headlines but not others. Reversing the list of headline beginnings fixes it (the original list makes goto-char fail as entries are inserted) Jul 10, 2019 at 17:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.