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.
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 afterfiletags
andhugo_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.
Relative Link Helpers
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
- use
C-c l
to insert a link - choose or type
ref:
orrelref:
- hit return, and navigate to the path of the other file under hugo’s
content
directory. - select the file I want
- 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.1ox-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.
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.
-
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. ↩︎