Quick script to refresh a Hugo Post Date
This is a short ๐ฉณ; a note that doesn’t merit a full post. See all shorts here.
I tend to go back to old, unpublished posts every now and restart my work on
them. They’re just resting, hiding behind the -D
flag of hugo server
.
But when I want to publish said post, I want the date to be the current date, not the creation date. So I wrote a script to update the date of a post to the current date.
Here’s the script in action:
โฏ go run main.go -file ../../content/posts/example.md -verbose
12:16PM DBG Current frontmatter Date=2024-04-27T12:10:21+03:00
12:16PM DBG After editing frontmatter date=2024-04-27T12:16:14+03:00
12:16PM INF Refreshed by diff=5m53s
12:16PM INF Successfully updated the date! ๐ post=../../content/posts/example.md
-date: "2023-12-10T21:33:29+02:00"
+date: "2024-04-27T12:16:14+03:00"
Here’s the code:
// A short script to update the date in the frontmatter of a Hugo post.
// The date is updated to the current date and time.
// Run this script with `go run scripts/refresh-post-date/main.go -file path/to/post.md`,
// and you'll see:
// > โฏ go run main.go -file path/to/post.md
// > 12:10PM INF Refreshed by diff=1m55s
// > 12:10PM INF Successfully updated the date! ๐ post=path/to/post.md
// You can add the `-verbose` flag to see more detailed logs.
package main
import (
"errors"
"flag"
"os"
"strings"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
)
const (
FrontmatterSeparatorLength = 3
HugoTimeFormatAsGoFormat = "2006-01-02T15:04:05-07:00"
)
func parseFrontmatter(fullPost []byte) ([]byte, error) {
fullPostAsString := string(fullPost)
// The frontmatter is between two lines of `---`
startIndex := strings.Index(fullPostAsString, "---")
if startIndex == -1 {
return nil, errors.New("failed to find the start of the frontmatter")
}
endIndex := strings.Index(fullPostAsString[startIndex+FrontmatterSeparatorLength:], "---")
if endIndex == -1 {
return nil, errors.New("failed to find the end of the frontmatter")
}
endIndex += startIndex + FrontmatterSeparatorLength
return []byte(fullPostAsString[startIndex:endIndex]), nil
}
func parseFrontmatterFromFullPostToMap(fullPost []byte) (map[string]any, error) {
frontmatterMap := make(map[string]any)
frontmatterContent, err := parseFrontmatter(fullPost)
if err != nil {
return nil, err
}
yamlErr := yaml.Unmarshal(frontmatterContent, &frontmatterMap)
if yamlErr != nil {
return nil, errors.Join(yamlErr)
}
return frontmatterMap, nil
}
func main() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) //nolint:exhaustruct
filePath := flag.String("file", "", "File path")
verbose := flag.Bool("verbose", false, "Verbose output")
flag.Parse()
if *filePath == "" {
flag.Usage()
log.Fatal().Msg("Please provide a file path")
}
if *verbose {
log.Logger = log.Level(zerolog.DebugLevel)
} else {
log.Logger = log.Level(zerolog.InfoLevel)
}
refreshDateInPost(*filePath)
}
// refreshDateInPost updates the date in the frontmatter of a Hugo post.
//
// Expects frontmatter in YAML format and between `---` separators.
// The date is updated to the current date and time, so make sure you're not in
// the middle of editing the post when running this script.
// See https://gohugo.io/content-management/front-matter/ for more information.
func refreshDateInPost(hugoPostFilePath string) {
content, err := os.ReadFile(hugoPostFilePath)
if err != nil {
log.Fatal().Err(err).Msg("Failed to read file")
}
currentFrontmatter, err := parseFrontmatterFromFullPostToMap(content)
if err != nil {
log.Fatal().Err(err).Msg("Failed to parse frontmatter")
}
currentDate := currentFrontmatter["date"]
log.Debug().Any("Date", currentDate).Msg("Current frontmatter")
newDate := time.Now().Format(HugoTimeFormatAsGoFormat)
currentFrontmatter["date"] = newDate
log.Debug().Any("date", currentFrontmatter["date"]).Msg("After editing frontmatter")
oldDateTime, err := time.Parse(HugoTimeFormatAsGoFormat, currentDate.(string))
if err != nil {
log.Fatal().Err(err).Msg("Failed to parse old date")
}
newDateTime, err := time.Parse(HugoTimeFormatAsGoFormat, newDate)
if err != nil {
log.Fatal().Err(err).Msg("Failed to parse new date")
}
diff := newDateTime.Sub(oldDateTime)
log.Info().Str("diff", diff.String()).Msg("Refreshed by")
newFrontmatter, err := yaml.Marshal(currentFrontmatter)
if err != nil {
log.Fatal().Err(err).Msg("Failed to marshal frontmatter")
}
oldFrontmatter, err := parseFrontmatter(content)
if err != nil {
log.Error().Err(err).Msg("Failed to parse frontmatter")
os.Exit(1)
}
newContent := strings.Replace(string(content), string(oldFrontmatter), "---\n"+string(newFrontmatter), 1)
err = os.WriteFile(hugoPostFilePath, []byte(newContent), 0o644) //nolint:gosec,gomnd
if err != nil {
log.Error().Err(err).Msg("Failed to write to file")
os.Exit(1)
}
log.Info().Str("post", hugoPostFilePath).Msg("Successfully updated the date! ๐")
}
And here’s a link to a gist as well.
Now, I can finally go to that old post I wanted to write and start working on it. …What was it about again? ๐ค
A rant about Go’s Time formatting
Go time formats bite me on the ass, AGAIN. grumble
I’ve proposed to support letter-based formats, such as “yyyy-mm-dd HH:MM:SS”
in time.Parse
. For more context
listen to this Cup o’ Go episode
where I talk about it.
What happened this time?
- Trying to write some code to mess around with my Hugo blog.
- Find this post and pull request from Jamie Tanna.
- OK, so I need ISO 8061 time format. SURELY I won’t have to write one out, right? Writing
time.I
in the IDE and waiting for the ISO to show up in the autocomplete… - OK, nothing… Let’s look it up online
- Debug some more, and discover I need an even more custom format (need to add a
:
in the timezone) - …
- Cry and copy-paste the string from StackOverflow since there’s no chance I’ll be able to write it first try.
Ended up with
const (
HugoTimeFormatAsGoFormat = "2006-01-02T15:04:05-07:00"
)