All posts

3 min read Nathan Broadbent

Adding a Timestamp to Hugo Post Filenames

An editor lists Markdown posts whose filenames begin with timestamps.

At the time of this post, the DocSpring blog was powered by Hugo, which is an awesome static site generator.

Update from April 2020: We migrated our blog to Ghost! Hugo is great, but we wanted to have a proper CMS where we could edit and publish blog posts.

Hugo is 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:

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

This helper uses macOS’s open command to launch the browser and editor. Replace those calls with the appropriate commands for another operating system.

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 used a script to rename all of my existing posts and set the original slug:

require 'yaml'
require 'date'
require 'time'
require 'json'

# Run from the directory containing the posts. Review the resulting git diff.
Dir.glob('*.md').sort.each do |path|
  next if File.basename(path).match?(/\A\d{14}-/)

  content = File.read(path)
  match = /\A---\r?\n(.*?)\r?\n---(?:\r?\n|\z)/m.match(content)
  raise "Missing YAML front matter: #{path}" unless match

  data = YAML.safe_load(match[1], permitted_classes: [Time, Date, DateTime])
  date = data.fetch('date')
  date = Time.iso8601(date) if date.is_a?(String)
  target = "#{date.strftime('%Y%m%d%H%M%S')}-#{path}"
  raise "File already exists: #{target}" if File.exist?(target)

  unless data.key?('slug')
    # JSON strings are also valid YAML strings, including quotes and spaces.
    slug = JSON.generate(File.basename(path, '.md'))
    content = content.sub(/\A---\r?\n/) { |opening| "#{opening}slug: #{slug}\n" }
    File.write(path, content)
  end
  File.rename(path, target)
end

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

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.

This post was written for Hugo v0.48 in 2018. Check the current replaceRE documentation when adapting the archetype.

More posts

  1. A cut ruby gemstone fitted exactly inside a navy blueprint outline, connected to four code cards with teal check marks, above a tag reading typed: strict.

    14 min read

    Going all-in on Sorbet

    Sorbet sat in our Rails app for 18 months with a tenth of the code typed. Then AI agents typed the rest in four days, and we kept going.

  2. An SDK card and an API card, each connected by a line to a filled PDF form

    4 min read

    PDF form filler SDK vs API

    When a PDF library is enough to fill your forms, and when a hosted API saves you the work.

  3. A JSON data card connected by a line to a PDF form with filled fields, a ticked checkbox and a signature

    5 min read

    Best PDF form filling APIs (2026)

    DocSpring, Anvil, PDF.co, Adobe PDF Services, and Nutrient compared for filling fillable PDFs and flat PDFs.

Try DocSpring with your own documents

Upload a PDF or a Word document, or create an HTML template, then send a test request. No credit card needed.

Blog posts and product news by email

New articles and product changes, once a month.