As mentioned when I first moved to use Org mode for blogging I haven't had a way to automatically create an index of posts. It's been a rather tedious manual task, and now that I've added TAGs and the RSS_PERMALINK property to the things I need to address I didn't want to do it manually any more. I took the opportunity to learn a little more Emacs lisp, and experiment with the Org APIs.

First and foremost I wanted a function that I could execute when visiting a blog post that generated the index entry for me, ready to paste into the index page. The parse-metadata function below is what I came up with. I query the Org parse tree for the title, date and category keywords, and the abstract special block. I created a little helper sb/org-kw-get function to avoid repetition, since I expected to extract three different keywords in the same way. (I've since decided not to extract DATE keywords). Figuring out how to extract the abstract was much harder, but I managed something that works well enough.

For generating the entry (in index.org) for my blog post I use Org mode programmatically. One caveat was that some of the APIs (I think most notably the org-set-tags-to call) would not work until I turned on Org mode in my temporary buffer. Instead of returning this as a string, I copy it to my kill ring so it's ready for pasting into the index page.

 1: (defun sb/org-kw-get (key)
 2:   "Return a lambda that takes an Org keyword
 3: element and returns its :value property if its :key
 4: property matches `key'."
 5:   `(lambda (kw)
 6:      (if (equal ,key (org-element-property :key kw))
 7:          (org-element-property :value kw))))
 8: 
 9: (defun sb/parse-metadata ()
10:   "Call in a blog post to get an entry suitable for
11: linking to this post from the index page."
12:   (interactive)
13:   (let* ((path (s-chop-prefix
14:                 (expand-file-name "~/blog/")
15:                 (buffer-file-name)))
16:          (tree (org-element-parse-buffer))
17: 
18:          (title
19:           (org-element-map tree 'keyword
20:             (sb/org-kw-get "TITLE") nil t))
21: 
22:          (categories
23:           (org-element-map tree 'keyword
24:             (sb/org-kw-get "CATEGORY")))
25: 
26:          (abstract
27:           (org-element-interpret-data
28:            (org-element-map tree 'special-block
29:              (lambda (sb)
30:                (if (equal "abstract"
31:                           (org-element-property :type sb))
32:                    (org-element-contents sb)))))))))
33: 
34: (with-temp-buffer
35:   (org-mode)
36:   (org-insert-heading)
37: 
38:   ;; Would have loved to use `org-insert-link' here but
39:   ;; I can't stop it from presenting a prompt :-(
40:   (insert "[[file:" path "][" title "]]")
41: 
42:   (insert "\n\n")
43:   (insert abstract)
44: 
45:   (org-set-property "PUBDATE" date)
46:   (org-set-property "RSS_PERMALINK"
47:                     (format "%s.html"
48:                             (s-chop-suffix ".org" path)))
49: 
50:   ;; Need to go back to the first line to set tags
51:   (goto-char (point-min))
52:   (org-set-tags-to categories)
53:   (org-set-tags nil t) ;; adjust tags in the source
54: 
55:   ;; Copy the contents of the temporary buffer as a string
56:   ;; *without properties* to my kill ring for pasting into
57:   ;; my index.org file
58:   (copy-region-as-kill
59:    (point-min) (point-max)))