FormAPI Blog

Adding a Timestamp to Hugo Post Filenames

The FormAPI blog is powered by Hugo, which is an awesome static site generator. It’s written with Go, so it’s incredibly fast. Generating the static pages for our blog only takes 139 ms.

When you create a new blog post, the convention is to use <slug>.md:

Hugo post filenames without any timestamps

This means that your posts will be sorted by alphabetical order in your text editor (I use VS Code). I wanted to sort my posts by date so that it would be easier to find recent posts.

To do this, I’ve written a new_post script that adds a timestamp to the beginning of the filename. (It also opens the URL in my browser, opens the file in my text editor, and starts the hugo server if it’s not already running.)

#!/bin/bash
set -e
cd blog
POST_SLUG="$1"
if [ -z "$POST_SLUG" ]; then
  read -p "Post Name (e.g. your-new-post): " POST_SLUG
fi
TIMESTAMP=`date +%Y%m%d%H%M%S`
POST_FILENAME="${TIMESTAMP}-${POST_SLUG}.md"
hugo new "posts/${POST_FILENAME}"
(
  sleep 0.2 &&
  echo "Opening localhost:1313/blog/posts/${POST_SLUG}/" &&
  open "http://localhost:1313/blog/posts/${POST_SLUG}/"
) &
(
  sleep 0.5 &&
  echo "Opening blog/content/posts/${POST_FILENAME}" &&
  open "content/posts/${POST_FILENAME}" &
) &
pgrep -x hugo > /dev/null && echo "Hugo is already running!" || hugo server -D
sleep 1

Running ./new_post test-post will generate a file at: ./blog/content/posts/20180924162015-test-post.md.

This gets us most of the way, but the default post title will be “20180924162015 Test Post”, and the URL will be: /blog/posts/20180924162015-test-post. I just want the timestamp in the filename, and I don’t want to include it in the title or the URL.

I updated my posts archetype (a.k.a. template) to strip the timestamp using the replace regex function (replaceRE.) Here’s my new blog/archetypes/posts.md:

---
title: "{{ .TranslationBaseName | replaceRE "^[0-9]{14}-" "" | replaceRE "-" " " | title }}"
slug: {{ .TranslationBaseName | replaceRE "^[0-9]{14}-" ""  }}
date: {{ .Date }}
draft: false
---

Hugo will generate a new post with the following contents:

---
title: "Test Post"
slug: test-post
date: 2018-09-24T16:20:15+07:00
draft: false
---

Setting the slug means that the post URL will be: /blog/posts/test-post

I also ran a Bash script to rename all of my existing posts and set the original slug:

for POST in *.md; do
  TIMESTAMP=$(ruby -r yaml -e \
    "puts YAML.load_file('$POST')['date'].strftime('%Y%m%d%H%M%S')")
  ! [[ $POST =~ ^[0-9]{14} ]] \
    && perl -i -p0e "s/(---\n\n)/slug: ${POST/.md/}\n\1/s" $POST \
    && mv $POST $TIMESTAMP-$POST
done

(I thought it might be interesting to talk about my process for writing one-liner scripts like this. I wrote another blog post about it, if you’re interested.)

Now my post filenames have timestamps, so they’re sorted by date and I can find the most recent post at the bottom:

Hugo post filenames with timestamps

Another little advantage of setting the slug is that it’s easier to copy/paste it. This helps when I’m creating links in different posts.

Note: I’m using the latest version of hugo (v0.48). Earlier versions of hugo may not have the replaceRE function.