Working with smaller chunks of thoughts; adding anchors to paragraphs in Org Mode HTML export

| org, js

I write my blog posts in Org Mode and export them to Eleventy with ox-11ty, which is derived from the ox-html backend.

Sometimes I want to link to something in a different blog post. This lets me build on thoughts that are part of a post instead of being a whole post on their own.

If I haven't added an anchor to the blog post yet, I can add one so that I can link to that section. For really old posts where I don't have an Org source file, I can edit the HTML file directly and add an id="some-id" so that I can link to it with /url/to/post#some-id. Most of my new posts have Org source, though. I have a my-blog-edit-org function and a my-blog-edit-html function in my Emacs configuration to make it easier to jump to the Org file or HTML for a blog post.

If the section has a heading, then it's easy to make that linkable with a custom name. I can use org-set-property to set the CUSTOM_ID property to the anchor name. For example, this voice access section has a heading that has CUSTOM_ID, as you can see in the . If I don't mind having long anchor names, I can use the my-assign-custom-ids function from my config to automatically set them based on the outline path.

my-assign-custom-ids
(defun my-assign-custom-ids ()
  (interactive)
  (let ((custom-ids
         (org-map-entries (lambda () (org-entry-get (point) "CUSTOM_ID")) "CUSTOM_ID={.}")))
    (org-map-entries
     (lambda ()
       (let ((slug
              (replace-regexp-in-string
               "^-\\|-$" ""
               (replace-regexp-in-string "[^A-Za-z0-9]+" "-"
                                         (downcase (string-join (org-get-outline-path t) " "))))))
         (while (member slug custom-ids)
           (setq slug (read-string "Manually set custom ID: ")))
         (org-entry-put (point) "CUSTOM_ID" slug)))
     "-CUSTOM_ID={.}")))

Adding anchors to paragraphs

If the part that I want to link to is not a heading, I can add an ID by using the #+ATTR_HTML: :id ... directive, like this snippet from my reflection on landscapes and art:

  #+ATTR_HTML: :id interest-development
  That reminds me a little of another reflection
  I've been noodling around on interest development...

Anchor links

It might be fun to have a little margin note with 🔗 to indicate that that's a specially-linkable section, which could be handy when I want to link when mobile. It feels like that would be a left margin thing on a large screen, so it'll just have to squeeze in there with the sticky table of contents. I've been meaning to add link icons to sub-headings with IDs, anyway, so I can probably solve both with a bit of Javascript.

/* Add link icons to headings and anchored paragraphs */
function addLinkIcons() {
  document.querySelectorAll('h1[id], h2[id], h3[id], p[id]').forEach((o) => {
    const link = document.createElement('a');
    const article = o.closest('article[data-url]');
    link.href = window.location.origin + (article?.getAttribute('data-url') || window.location.pathname) + '#' + o.getAttribute('id');
    link.innerHTML = '🔗';  // link icon
    link.title = 'anchor';
    link.classList.add('anchor-icon');
    link.addEventListener('click', copyLink);
    o.prepend(link);
  });
}

addLinkIcons();

And some CSS:

.entry h2, .entry h3, .entry h4, .entry p, .content h2, .content h3, .content h4, .content p { position: relative }
.content a.anchor-icon:link, .entry a.anchor-icon:link { font-size: 60%; text-decoration: none !important; font-size: small; float: right }
@media only screen and (min-width: 95rem) {
   .content a.anchor-icon:link, .entry a.anchor-icon:link { display: block; position: absolute; left: -2em; top: 0.2em; float: none}
}

Text fragments

Text fragments are even more powerful, because I can link to a specific part of a paragraph. I can link to one segment with something like #::text=text+to+highlight~. I can specify multiple text fragments to highlight by using #::text=first+text+to+highlight&text=second+text~, and the browser will automatically scroll to the first highlighted section. I can specify a longer section by using text=textStart,textEnd. Example: #:~:text=That%20is%20the%20gap,described The text fragments documentation has more options, including using prefixes and suffixes to disambiguate matches.

Text fragment links require rel="noopener" for security, so I added JKC-Codes/eleventy-plugin-automatic-noopener to my 11ty config.

If I find myself using this a lot, it might be interesting to figure out how to make it easier, maybe with something like the Text Fragment extension for Firefox.

These seem like good starting points for addressing smaller chunks of thoughts.

View org source for this post
You can comment with Disqus or you can e-mail me at sacha@sachachua.com.