Go Time

This is one of those posts about a tech issue comes up from time to time for me that I find difficult to search for. Sorry (not sorry) if it is not helpful to anyone else!

Too much context about my site building process

My site is built with Hugo, a static site generator known for being quite fast, and with a powerful (and challenging) templating system.

I use my site for lots of stuff, which sometimes means showing content from other sites in my posts. For example:

In either case, my site's build process includes a step where it finds new links that need this context - to be displayed as a preview or displayed as a like/comment/etc., and it fetches that stuff. To do this, my site fetches the link content, then parses it using a handy library called aaronpk/XRay, then saving it in a place where Hugo can find it at build time.

For example, the RSVP post I mentioned gets a little chunk of data like this:

{
  "fetched_at": "2019-10-02T23:29:15-04:00",
  "xray": {
    "data": {
      "type": "entry",
      "published": "2019-10-02 19:53-0700",
      "url": "https://tantek.com/2019/275/t1/indiewebcamp-new-york-city",
      "content": {
        "text": "going to #IndieWebCamp NYC this weekend ...",
...

I’ve cut out a lot of stuff here and highlighted the two things relevant for my particular time issue.

In my templates for previews and responses I try to show the published time from the post itself. If XRay wasn't able to find a value for published or (spoiler) Hugo can't parse that value as a time, the template will give up and omit the published time.

What is Web Time?

This is such a ridiculously fraught question that I refuse to answer it properly. Suffice to say:

What is (Hu)go Time?

Okay, finally, the point of this post has been found. To archive this lil' set of facts for my future frustrations.

From an unrelated Hugo date parsing issue, I learned about the list of time formats that Hugo tries when you ask it to parse something as a time, last updated August 2022:

var (
  timeFormats = []timeFormat{
    {time.RFC3339, timeFormatNumericTimezone},
    {"2006-01-02T15:04:05", timeFormatNoTimezone}, // iso8601 without timezone
    {time.RFC1123Z, timeFormatNumericTimezone},
    {time.RFC1123, timeFormatNamedTimezone},
    {time.RFC822Z, timeFormatNumericTimezone},
    {time.RFC822, timeFormatNamedTimezone},
    {time.RFC850, timeFormatNamedTimezone},
    {"2006-01-02 15:04:05.999999999 -0700 MST", timeFormatNumericAndNamedTimezone}, // Time.String()
    {"2006-01-02T15:04:05-0700", timeFormatNumericTimezone}, // RFC3339 without timezone hh:mm colon
    {"2006-01-02 15:04:05Z0700", timeFormatNumericTimezone}, // RFC3339 without T or timezone hh:mm colon
    {"2006-01-02 15:04:05", timeFormatNoTimezone},
    {time.ANSIC, timeFormatNoTimezone},
    {time.UnixDate, timeFormatNamedTimezone},
    {time.RubyDate, timeFormatNumericTimezone},
    {"2006-01-02 15:04:05Z07:00", timeFormatNumericTimezone},
    {"2006-01-02", timeFormatNoTimezone},
    {"02 Jan 2006", timeFormatNoTimezone},
    {"2006-01-02 15:04:05 -07:00", timeFormatNumericTimezone},
    {"2006-01-02 15:04:05 -0700", timeFormatNumericTimezone},
    {time.Kitchen, timeFormatTimeOnly},
    {time.Stamp, timeFormatTimeOnly},
    {time.StampMilli, timeFormatTimeOnly},
    {time.StampMicro, timeFormatTimeOnly},
    {time.StampNano, timeFormatTimeOnly},
  }
)

There's a lot of time.Whatever constants in that list. These are part of the Go language's time package :

const (
  Layout = "01/02 03:04:05PM '06 -0700" // The reference time, in numerical order.
  ANSIC = "Mon Jan _2 15:04:05 2006"
  UnixDate = "Mon Jan _2 15:04:05 MST 2006"
  RubyDate = "Mon Jan 02 15:04:05 -0700 2006"
  RFC822 = "02 Jan 06 15:04 MST"
  RFC822Z = "02 Jan 06 15:04 -0700" // RFC822 with numeric zone
  RFC850 = "Monday, 02-Jan-06 15:04:05 MST"
  RFC1123 = "Mon, 02 Jan 2006 15:04:05 MST"
  RFC1123Z = "Mon, 02 Jan 2006 15:04:05 -0700" // RFC1123 with numeric zone
  RFC3339 = "2006-01-02T15:04:05Z07:00"
  RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00"
  Kitchen = "3:04PM"
  // Handy time stamps.
  Stamp = "Jan _2 15:04:05"
  StampMilli = "Jan _2 15:04:05.000"
  StampMicro = "Jan _2 15:04:05.000000"
  StampNano = "Jan _2 15:04:05.000000000"
  DateTime = "2006-01-02 15:04:05"
  DateOnly = "2006-01-02"
  TimeOnly = "15:04:05"
)

If your brain just can't help but puzzle-solve, you may have noticed that the published time from the example above:

"2019-10-02 19:53-0700"

Is close to but does not match either the time.RFC3339 or time.DateTime formats.

The issue? Neither Hugo nor Go fully support ISO 8601 time, instead supporting very close time formats which do not allow omitting the seconds value. There are definitely other formats they don't support, which I've seen commonly, like "January 2, 2006".

Workaround (Deprecated)

When I first built out the Hugo templates for my site (2018, so back in the 0.4x or 0.5x days, maybe?), if you asked Hugo to parse a time, and it couldn't, it would give you back a string instead with an error message beginning "unable to parse date: ...". This was gnarly, but I could code around it by asking Hugo to convert a value to a time and checking if the result starts with "unable to parse". Something like:

{{ $safe_published := (time $item_published) -}}
{{ if not (hasPrefix $safe_start "unable to parse") -}}
  {{/* ... it parsed, hooray, use it */}}
{{ else -}}
  {{/* failed to parse so do a fallback or ignore it or whatever */}}
{{ end -}}

Time passes and Hugo has since changed this behavior. Now, if you ask it to convert a value to a time, and it cannot parse it with one of the known format strings, Hugo bails and the entire build fails. It throws an error something like:

ERROR 2023/03/19 13:02:05 render of "page" failed:
  "/home/schmarty/me/martymcgui.re/themes/mmgre-2015/layouts/_default/single.html:3:7":
  execute of template failed: template: _default/single.html:3:7:
  executing "main" at <partial "post/post.html" (...)>:
  error calling partial: "/home/schmarty/me/martymcgui.re/themes/mmgre-2015/layouts/partials/post/post.html:20:36":
  execute of template failed: template: partials/post/post.html:20:36:
  executing "partials/post/post.html" at <partial "link-preview/link-preview.html" (...)>:
  error calling partial: "/home/schmarty/me/martymcgui.re/themes/mmgre-2015/layouts/partials/link-preview/link-preview.html:26:9":
  execute of template failed: template: partials/link-preview/link-preview.html:26:9:
  executing "partials/link-preview/link-preview.html" at <partial (printf "link-preview/%s.html" "xray") (...)>:
  error calling partial: "/home/schmarty/me/martymcgui.re/themes/mmgre-2015/layouts/partials/link-preview/xray.html:30:28":
  execute of template failed: template: partials/link-preview/xray.html:30:28:
  executing "partials/link-preview/xray.html" at <partial "util/safe-time.html" $item.published>:
  error calling partial: "/home/schmarty/me/martymcgui.re/themes/mmgre-2015/layouts/partials/util/safe-time.html:1:4":
  execute of template failed: template: partials/util/safe-time.html:1:4:
  executing "partials/util/safe-time.html" at <time .>:
   error calling time: unable to parse date: 2019-10-02 19:53-0700

Believe it or not I actually cleaned up that error a lot, removed duplicates, and highlighted the key issue in bold.

I hate this.

Workaround (Derogatory)

For now I check my site's build logs from time to time looking for build errors like the one above. I'll then:

  1. Use a tool like grep to search my saved reply context data for the problematic time string (in this case "2019-10-02 19:53-0700").
  2. Find the file it's in, and "fix" it so Hugo can parse it. In this case, "2019-10-02 19:53:00-0700".
  3. With the "fix" in place, I'll rebuild the site.

Either it builds successfully (hooray!) or I find the next time parsing failure and repeat.

I seriously dislike this.

A Future Fix

What I actually need to do here is update my build system to sanitize the value of published if it exists.

Like Hugo's list, I'll need to decide what date string formats I want to support, have the build system try them all on that published value, and store a sanitized version for Hugo.

Okay! Now I have this post to refer to in the future when I get next get cranky about this.