Managing drafts in Hugo

I’ve written before about how the Hugo static site generator’s flexible content model allows me to do some interesting things that would be more difficult or impossible in a more blog-centric system such as Ghost. On the flipside, Hugo also lets me do things that would be a lot easier in a more blog-centric system… but with a bit more effort. Case in point: draft management.

Every post on this blog is a .md file in content/post/. Filenames (usually) correspond to URL slugs – for example, this post is a file called managing-drafts-in-hugo.md. Unfortunately, things gets unwieldy when you have 117+ posts, a random smattering of which are drafts. Unlike in Jekyll, post filenames don’t start with the date,1 so post/ is in a useless alphabetical order.2 There’s also no way to tell which posts are drafts without reach for grep. But rather than do that, I decided to use Hugo’s own organisational power to manage my drafts.

In Hugo, any content type can be a draft: you just have to add draft = true its frontmatter. All this does is prevent Hugo from including the file unless you pass the option --buildDrafts to it. In light of this, I made a Drafts page, accessible at /drafts/, and permanently marked as a draft (so don’t try navigate to it).

Thus, this Drafts page acts as an index of all my drafts that’s only accessible in --buildDrafts mode. Since Hugo version 0.52, it’s been possible to create inline shortcodes, which allow content files to hook into Hugo’s “server-side” processing in ad-hoc ways. Note that this feature is disabled by default and may present a security risk if you do not have full oversight of your site’s content files.

My inline shortcode for printing a draft index looks like this:

{{< drafts.inline >}}
  <h1>posts</h1>
  <div class="nav-list-headingless">
  {{ range (where .Site.RegularPages "Type" "post") }}
    {{ if .Draft }}
      {{ partial "post-list-item-small.html" . }}
    {{end}}
  {{ end }}
  </div>

  <h1>pages</h1>
  <div class="nav-list-headingless">
  {{ range (where .Site.RegularPages "Type" "page") }}
    {{ if .Draft }}
      {{ partial "post-list-item-small.html" . }}
    {{end}}
  {{ end }}
  </div>

  <h1>fiction</h1>
  <div class="nav-list-headingless">
  {{ range (where .Site.RegularPages "Type" "fiction") }}
    {{ if .Draft }}
      {{ partial "post-list-item-small.html" . }}
    {{end}}
  {{ end }}
  </div>
{{< /drafts.inline >}}

I use a specific partial to display each item as a box with a background image, but you replace that with a simple link if you want to keep things simple. I also split up my site’s three main content types (date-stamped blogposts, utility pages and fiction) which you may not need or want to do.

<a href="{{ .RelPermalink }}">{{ .Title }}</a>

If you don’t want to enable inline shortcodes, you can also just make a shortcode called drafts.html with the content above and include it in your Drafts page like this:

{{< drafts >}}

This feels a bit hacky, and there’s probably a more kosher way to do this with layouts and other Hugo internals, but it works. And once you have a basic drafts page, it’s easy to use Hugo’s organisational features to order and group drafts by taxonomies, date, content type, or other attributes. Having a page that shows all my drafts organised like this makes it easier for me to remember what subjects I’ve started writing about in the past and come back to promising ones.


  1. Which is not to say putting dates in filenames would be impossible in Hugo, just that it’s not the default behaviour. ↩︎

  2. Sorting the folder by date created or modified wouldn’t really help, as I sometimes create posts months before publishing them, and some may be post-dated or edited to fix spelling mistakes after publication. ↩︎

similar posts

webmentions(?)