Blog from Org-Mode to Hugo

I use the static web-site generator Hugo to create my home page. I also use Emacs as my main editor. Hugo is good with Markdown. Emacs is good at Markdown, too. But much better with Org-Mode.

If you want …

  • export one .org file as one web page, look at Giles Paterson solution
  • export just a subtree of an org-file (e.g. from your org-based Emacs configuration), then look here.

I wanted a nice way to publish single sub-trees of an org-file to Hugo. So I wrote my own “publish this specific subtree” export. The interactive function is simply called hugo, and I bind it to some key combination, in my case to Alt-g h, g like go, and h like go. So I type “go hugo”, more or less.


One question is how we store Hugo-specific information. So far I only care for the title, tags, topics and, of course, the file name. As I don’t want to have one-file per blog post, but instead use subtrees of my org-file, I need to store this information into org’s property drawers.

But writing them by hand is tedious. So I added code that ensures that all needed properties exists. Before I started blogging this article, my org-mode buffer looked like this:


Then I called the (hugo) function and my buffer looked like this:


The cursor is positioned at the first empty field.

(Note that I later changed the code below from HUGO_CATEGORIES to HUGO_TOPICS, because that’s how I now have defined my taxonomy in Hugo.)

Note that the title and date fields are pre-filled. You can of course change them. Only when everything is filled in …


… does the export to Hugo create the markdown file with the properly formatted TOML front matter.


Let’s start simple. First we define where our contents should be stored:

;; This is GPLv2. If you still don't know the details, read

(defvar hugo-content-dir "~/www.hugo/content/"
  "Path to Hugo's content directory")

The next two functions care that all needed property drawers exist:

;; This is GPLv2. If you still don't know the details, read

(defun hugo-ensure-property (property)
  "Make sure that a property exists. If not, it will be created.

Returns the property name if the property has been created,
otherwise nil."
  (if (org-entry-get nil property)
    (progn (org-entry-put nil property "")

(defun hugo-ensure-properties ()
  "This ensures that several properties exists. If not, these
properties will be created in an empty form. In this case, the
drawer will also be opened and the cursor will be positioned
at the first element that needs to be filled.

Returns list of properties that still must be filled in"
  (require 'dash)
  (let ((current-time (format-time-string (org-time-stamp-format t t) (org-current-time)))
      (unless (org-entry-get nil "TITLE")
        (org-entry-put nil "TITLE" (nth 4 (org-heading-components))))
      (setq first (--first it (mapcar #'hugo-ensure-property '("HUGO_TAGS" "HUGO_TOPICS" "HUGO_FILE"))))
      (unless (org-entry-get nil "HUGO_DATE")
        (org-entry-put nil "HUGO_DATE" current-time)))
    (when first
      (goto-char (org-entry-beginning-position))
      ;; The following opens the drawer
      (forward-line 1)
      (beginning-of-line 1)
      (when (looking-at org-drawer-regexp)
        (org-flag-drawer nil))
      ;; And now move to the drawer property
      (search-forward (concat ":" first ":"))

And this is the main function. It simply gathers all information from org-mode, formats it correctly, and writes it out.

In case you have the ox-gfm.el elisp package is available, the export will use “Github Flavored Markdown”. Otherwise, the normal markdown export backend will be use. The benefit of =’gfm= is that code blocks can be highlighted.

;; This is GPLv2. If you still don't know the details, read

(defun hugo ()
  (unless (hugo-ensure-properties)
    (let* ((title    (concat "title = \"" (org-entry-get nil "TITLE") "\"\n"))
           (date     (concat "date = \"" (format-time-string "%Y-%m-%d" (apply 'encode-time (org-parse-time-string (org-entry-get nil "HUGO_DATE"))) t) "\"\n"))
           (topics   (concat "topics = [ \"" (mapconcat 'identity (split-string (org-entry-get nil "HUGO_TOPICS") "\\( *, *\\)" t) "\", \"") "\" ]\n"))
           (tags     (concat "tags = [ \"" (mapconcat 'identity (split-string (org-entry-get nil "HUGO_TAGS") "\\( *, *\\)" t) "\", \"") "\" ]\n"))
           (fm (concat "+++\n"
           (file     (org-entry-get nil "HUGO_FILE"))
           (coding-system-for-write buffer-file-coding-system)
           (backend  'md)
      ;; try to load org-mode/contrib/lisp/ox-gfm.el and use it as backend
      (if (require 'ox-gfm nil t)
          (setq backend 'gfm)
        (require 'ox-md))
      (setq blog (org-export-as backend t))
      ;; Normalize save file path
      (unless (string-match "^[/~]" file)
        (setq file (concat hugo-content-dir file))
      (unless (string-match "\\.md$" file)
        (setq file (concat file ".md")))
      ;; save markdown
        (insert fm)
        (insert blog)
        (untabify (point-min) (point-max))
        (write-file file)
        (message "Exported to %s" file))

And finally I set my preferred key-binding with (bind-key). I like this method over over the standard keybinding methods because it works together with (describe-personal-keybindings). And because I’m already a use-package user it’s already loaded anyway :-)

(bind-key "M-g h" #'hugo)


In this blog post are 3 pictures. My current code does not copy them to Hugo’s contents directory, and I don’t really plan this. I don’t want to maintain images as part of my Emacs configuration. Instead, I added text like this into my org-mode buffer and do the rest in my Hugo setup:


Separate description (teaser) from main content

In Hugo, you can separate your normal content from he teaser at the top with a “<!–more–>” marker. Generate this HTML with “#+HTML: <!–more–>” in a line by itself.