Using Org Mode With Hugo

Overview

This post is for people who already understand why org-mode is the “greatest thing since sliced bread” & want to build a static web site with Hugo.

Hugo’s built-in org-mode support

Hugo supports org-mode files by default thanks to the go-org library. It will allow you to create blog posts with .org files and .md files. However I recommend that you don’t use this option.

go-org has the same problem that every non-Emacs implementation of org has. It doesn’t implement support for everything org-mode can do, and org-mode can do a lot. It’s mostly good, but mostly only gets you most of your needs, not all of them. It’s one of those situations where it seems good, until you start stumbling across little things that don’t work right or aren’t supported. The more you post, the more likely it is to frustrate you in the long run.

Ox-Hugo

I recommend you us ox-hugo. It’s an excellent org-mode exporter that takes your org-mode file, converts it to Hugo friendly Markdown with TOML frontmatter. It can be configured to use YAML frontmatter but you’ll probably never look at the generated file so I don’t know why you would.

Basic Usage

The idea is pretty simple. You have some org-mode files of blog posts you’re working on. When you’re ready, you tell ox-hugo to export them, and it generates Hugo-friendly Markdown files.

You can also have it create a blog post from a “subtree” of an org-mode file. That is to say “This heading and everything under it should be a blog post”.

You’ll want to keep your org-mode files in a directory other than Hugo’s content directory, because we don’t want it to even think about your org-mode files. We only want it to consider the Markdown files we generate.

At the root of my hugo site I have a proto_posts directory where I create all my new blog posts. It doesn’t matter what you call it, and it doesn’t matter if you use a directory structure inside of it. It doesn’t even matter if you have it in your hugo site. You could even keep all your writing in Denote or Org-roam collections.

Configuration

Because the org-mode files could be literally anywhere on your computer, you need to tell ox-hugo what directory to export to.

.dir-locals

If you’re writing them to a directory within your hugo folder like I am, then you can add a .dir-locals.el file at the root of your hugo folder with the following content.

((nil . ((org-hugo-base-dir . "~/path/to/weblog.masukomi.org/"))))

This is an especially nice way of handling it if you’re managing multiple sites. Each one can have its own .dir-locals.el file to make sure your proto_posts always end up in the correct content directory.

keyword

There are cases where using a .dir-locals file isn’t an option. For example, you might use Denote or Org-roam for all your writing and want to export to multiple blogs from that. In that case you’ll need to use a keyword in your org-mode metadata (frontmatter)

#+hugo_base_dir: ~/path/to/my/site

Helpful Bits & Info

Where the post goes

Most hugo sites are broken up into “sections”. Typically it’s one collection of “pages” that live at the top level. Things like your “about” page. The files for these typically live under content/

Usually there’s a collection of “posts” too. The posts are what we’re typically interacting with when we visit a blog and these live under content/post/

In order to control where your exported file goes we use the hugo_section keyword. Most of the time you’ll be creating a post, so most of the time it looks like this.

#+hugo_section: post

But if you needed a file at the top level you’d use this

#+hugo_section: /

It can be any folder name or even subtrees like post/journal/2024 depending on the theme you’re using for your Hugo site.

Frontmatter Helpers

ox-hugo tries really hard to not interfere with common org-mode metadata keywords. The official ox-hugo keywords are listed here. In practice, I suspect that most people will have some form of auto-generated metadata like I show below, so it’s pretty rare that you’ll have to think about frontmatter.

Fortunately for us ox-hugo’s creator gave emacs everything it’d need to populate the autocomplete with all the options. Just type #+hugo_ and look through the list.

a screenshot of the autocomplete menu shown after typing pound plus hugo underscore

Auto-generated metadata

Auto-generated metadata (frontmatter) is a massive time-saver. So the first thing I do when I open a new file for a blog post is to type hfm to invoke my “Hugo FrontMatter” snippet (using YASnippet of course).

This sets up all the frontmatter I need, & generates the date strings.

# -*- mode: snippet -*-
# name: hugo-frontmatter
# key: hfm
# --
#+author: masukomi
#+hugo_publishdate: `(format-time-string "%Y-%m-%dT%T%z")`
#+date: `(format-time-string "%Y-%m-%dT%T%z")`
#+hugo_auto_set_lastmod: t
#+filetags: $1
#+toc: headlines 3
#+draft: false
#+hugo_section: ${2:post}
#+hugo_custom_front_matter: :summary "I NEED A SUMMARY"
  • Things to note about that snippet

    There is no #+title: … line. This is because my process is such that whenever I open a new .org file for blogging it auto-populates with a name generated from the file name. If your process doesn’t do that, then add a #+title: $1 line above the #+author line, and be sure to increment the numbers after filetags and hugo_section

    For those who don’t know the dollar-sign numbers in YASnippet are essentially fields you fill in and tab to the next one. Your cursor will jump through them in numerical order starting with 1.

    The other thing to note is the #+toc: headlines 3 This is for ox-hugo’s table of contents generation which is arguably superior to the TOC generation that Hugo has built in. If you don’t want a table of contents on your post, just delete that line.

    To be explicit, this is separate functionality from that provided by toc-org.

Image & Alt Text

All you need to do to insert an image is to create a file: link to something with a common image file extension like .jpg or .png. It’ll just do the right thing.

The problem with this is that it provides no alt-text for blind readers and you might want a descriptive title for the image.

To address these we use the ATTR_HTML keyword with :alt or :title respectively. These aren’t ox-hugo specific. They’re what you’d use if you had an org document that was going to ultimately be converted to HTML.

For example:

#+ATTR_HTML: :alt a cute black and white puppy being cradled in my lap
#+ATTR_HTML: :title our new puppy
[[file:/images/path/to/puppy.jpg]]

That would get converted to:

{{< figure src="/images/path/to/puppy.jpg" alt="a cute black and white puppy being cradled in my lap" title="our new puppy" >}}

ox-hugo is using Hugo’s figure shortcode, because Markdown doesn’t actually support alt tags, or titles

Note that, just like in normal Hugo Markdown, the path you use needs to be the path that will work on the final generated site. For example: the images on this site are under /images but in my hugo folder they’re under static/images/ Whenever I insert an image I have to use a /images/… path to make it work in the final rendered site.

I’m sure someone could combine some elisp and the hugo_base_dir info to enable the images to be displayed inline but I don’t really care.

  • My Hugo Image Snippet

    I don’t like having to worry about unique image names. I always keep the images for a post in a dated folder that matches the post’s date. For example: this posts images are in a folder named /images/2024/07/19/

    When I want to insert an image into the page I type himage to invoke the following snippet that generates the alt and title keywords + pre-populates the image url with a date based folder path.

    # -*- mode: snippet -*-
    # name: hugo-image
    # key: himage
    # --
    
    #+ATTR_HTML: :alt $2
    #+ATTR_HTML: :title
    [[file:`(format-time-string "/images/%Y/%m/%d/")`$1]]
    

    Note that the cursor starts inside the link path because I’ve usually got the image filename on my clipboard. I can then just paste, & tab over to start writing the alt text.

    Most of the time I delete the title line.

Cross-referencing other pages on your site is pretty common. The problem that Hugo is solving with ref & relref shortcodes is that, without reading the frontmatter of the file you’re linking to, & pondering how Hugo will interpret it, you don’t know the final url of the page you’re linking to. For example: pages on this site are all prefaced with year, month, and date that the post was initially published, but that’s all calculated from the frontmatter during Hugo’s build process. All I know when writing is that it’s generated from a file called “my_relative_file.md” in the same content/post/ directory that the file I’m linking from will be in.

Shortcode handling is a little problematic in ox-hugo. See Shortcodes below. To create a relref link you’d have to write something like this.

@@hugo:[foo]({{< relref "my_relative_file.md" >}})@@
OR
[foo](@@hugo:{{< relref "my_relative_file.md" >}}@@)

I don’t like having to type that obnoxious line and there’s a reasonable chance I’ll make a typo or misremember something when writing the filename.

So, I’ve added a couple functions to my emacs config to make inserting a rel or relref link work just like inserting any other link.

Now I can

  1. use C-c l to insert a link
  2. choose or type ref: or relref:
  3. hit return, and navigate to the path of the other file under hugo’s content directory.
  4. select the file I want
  5. enter a description.

When I export it will convert it to a markdown style link with the ref / relref shortcode for the url.

;; New link type for Org-Hugo internal links
(with-eval-after-load 'ox-hugo
  (org-link-set-parameters
   "relref"
   :complete (lambda ()
               (concat
                "relref:"
                (file-name-nondirectory (read-file-name "File: "))
                )
               )

   :export (lambda (path description backend)
             (format "[%s]({{< relref %s >}})" description path  )
        )
   )

  (org-link-set-parameters
   "ref"
   :complete (lambda ()
               (concat
                "ref:"
                (file-name-nondirectory (read-file-name "File: "))
                )
               )

   :export (lambda (path description backend)
             (format "[%s]({{< ref %s >}})" description path  )
        )
   )

)

Note that there is no :follow function set. Clicking on these links in org-mode won’t go anywhere. This is intentional because the link is going to the markdown file used when the site is generated, but I don’t know where the org-mode file used to generate that markdown file is, and that is the file you’d want to open.

I’ve considered changing rel: & relref: to hugorel: & hugorelref: but it’s not currently interfering with anything. If you do change it to something else be sure that the initial string and the text after concat match each other.

Things to watch out for

Frontmatter

I expected that it’d just convert all the org-mode metadata / frontmatter, but it defaults to only converting a common subset of it, that it knows Hugo will want. Here’s a table documenting those.

I assume that the thinking is that org-mode files can have a lot of frontmatter that has nothing to do with your eventual blog-post.

There are two ways to solve this. For most people who need a non-standard keyword you’ll just want to add this to your org mode frontmatter. You’d just change :key and "value" to whatever hugo frontmatter key & value you want.

#+hugo_custom_front_matter: :key "value"

If you’re converting subtrees of a larger document into individual posts then you can use Properties. This is documented in the Org meta-data to Hugo front-matter post.

  • summary keyword

    ox-hugo doesn’t pass on the summary keyword which many Hugo themes use to display a short summary under your posts. Without this, most Hugo themes will just use the first bit of text in the post. If you use Tables of Contents, like I do in most of my posts, then that’s going to produce terrible summaries.

  • tags

    offically you should use #+hugo_tags: tag_a tag_b but I’ve been using #+filetags and that converts just fine.

Shortcodes

  • Using Them

    ox-hugo is good, but Hugo’s shortcodes are a problem because {{< … >}} and {{% … %}} are not legal Org syntax.1

    ox-hugo has multiple ways of addressing this. Most blog posts don’t really use shortcodes much. The most you’ll probably want on a semi-regular basis is something quick and inline rather than a multi-line block you can use these handy escapes.

    @@hugo:..@@ <-- for Hugo templating stuff
    @@md:..@@   <-- for raw Markdown stuff
    @@html:..@@ <-- for raw HTML stuff
    

    For example, if I wanted to create a relref link i could say

    @@hugo:[foo]({{< relref "my_relative_file.md" >}})@@
    OR
    [foo](@@hugo:{{< relref "my_relative_file.md" >}}@@)
    

    Basically it’s saying “Just let hugo deal with whatever’s in here.”

    Because creating links to other posts on your site is a pretty common thing I’ve created a couple helper functions to make this less prone to typos & more natural feeling to org-mode users. See Relative Link Helpers.

  • Blogging About Shortcodes

    Documenting the markup of a language without invoking the markup of the language is a pain in most (all?) markup languages. In this case we’re actually dealing with two layers of this. We don’t want to have the shortcodes invoked in the exported file, and we don’t want any org-mode syntax (like asterisks for bolding) to be invoked in the org-mode file.

    Chris Liatas discovered the simple solution of sticking a comment inside the shortcode.

    a screenshot showing how to insert a comment inside a shortcode in order to trick hugo into showing the shortcode and then how to trick org-mode into not using the asterisks in that comment as bolding indicators

    So everywhere in this document where you see something like {{< … >}} or {{% … %}}

    What I’ve actually typed in the original is two curlies, the < or %, then /* … */, and then the closing characters for the shortcode.

org-entities

Org-mode has built in support for laTex style special characters. John Cook has a nice blog post that helps explain why these are potentially useful. As a simple example \ast represents an asterisk.

In writing this post I discovered that ox-hugo does not convert them. So you end up with a literal \ast in your markdown. I’ve filed a bug documenting the problem. If you’re reading at some point after July 2024, and want to use org-entities you should check the status of that ticket.


  1. I don’t really understand why this is illegal syntax since org-mode doesn’t seem to complain when I enter them but it doesn’t really matter. What matters is that they give ox-hugo headaches. ↩︎