Every couple of years months [checks wristwatch] weeks, we reinvent a file format for no particularly good reason. Don’t get me wrong; we come up with all kinds of reasons to justify what we’re doing—easier to read, better for the environment, It’s Got Electrolytes™—and sometimes, the new format does genuinely represent a meaningful or necessary improvement. But more often than not, we’re just reinventing things out of boredom and a nagging sense, deep down, that if we don’t keep changing everything constantly, normal people may grok that most of the reason programming is complicated and weird is because we put a lot of effort into making it that way.

So I was pretty psyched when JSON Feed came on the scene a couple weeks ago, because it’s pretty much the absolute rawest possible example of a file format that’s unrepentantly change for the sake of change. Literally every language I interact with has perfectly good tools, right in the standard library, for generating and consuming RSS and Atom. Until a few weeks ago, none had any tools for working with JSON Feed whatsoever because it didn’t even exist. But since, and I quote from the JSON Feed manifesto, “developers will often go out of their way to avoid XML,”[1] JSON Feed is now a thing, and we’ve already entered the phase where every language I use has a pile of third-party libraries for the format, most of which will be unsupported going forward, and all of which have interesting quirks and bugs that no one fully understands yet. I thus figured it was high time to support JSON Feed on bitquabit.

There was unfortunately a caveat. Some time ago, I moved my blog over to Hugo, a static site generator, so that I wouldn’t have to spend time maintaining my own blog software. In general, that’s been brilliant, but whereas it’d have taken me about five minutes to add JSON Feed to my old blog, I had no idea how to add it to a Hugo site. The highest-ranked link on Google is just vague enough to make me think I should get it but not be able to, and I can say in retrospect that Hugo’s documentation on alternate output formats makes a ton of sense after you already know what’s going on—but not before.

So without further ado, here’s how you add JSON Feed to a Hugo site:

Add some magic to config.toml

We want to tell Hugo that there’s a thing called JSON Feed, which is a JSON file, and we want to assign it a file extension. That’s easy enough. In your config.toml, just slam the following lines at the end:

[outputFormats.jsonfeed]
  mediaType = "application/json"
  baseName = "feed"
  isPlainText = true

mediaType is the file’s MIME type, baseName is just the name of the file template before the extension[2], and isPlainText tells Hugo that it shouldn’t do any HTML-related shenanigans. Whatever you slap after the . in outputFormats at the beginning, combined with the media type, defines the expected file extension, so everything we just wrote applies to files that end with .jsonfeed.json. Putting everything together, we’ve now told Hugo that feed.jsonfeed.json files are JSON Feed templates. So far, so good.

Next up, we tell it that we would like it to generate a JSON Feed if one exists. If you already have a section in your config.toml labeled [outputs] (you don’t by default), you’ll need to alter it, but otherwise you can just this at the end:

[outputs]
  home = ["html", "jsonfeed", "rss"]

All that says is, “hey, when you’re generating my home page, in addition to HTML and RSS (which are defaults), also generate this "jsonfeed" thing,” which (conveniently) we just defined.

Add a template for the JSON Feed

We told Hugo that our JSON Feed templates would end in jsonfeed.json and that the base name would be feed, so go create a file called feed.jsonfeed.json in the root of your content/ directory and put this in it:

{
  "version": "https://jsonfeed.org/version/1",
  "title": "{{ .Site.Title }}",
  "home_page_url": {{ .Permalink | jsonify }},
  "feed_url": {{ with .OutputFormats.Get "jsonfeed" -}}
    {{- .Permalink | jsonify -}}
  {{- end }},
  "items": [
    {{ range $index, $entry := first 15 .Data.Pages }}
    {{- if $index }}, {{ end }}
    {
      "id": {{ .Permalink | jsonify }},
      "url": {{ .Permalink | jsonify }},
      "title": {{ .Title | jsonify }},
      "date_published": {{ .Date.Format "2006-01-02T15:04:05Z07:00" | jsonify }},
      "content_html": {{ .Content | jsonify }}
    }
    {{- end }}
  ]
}

Most of that’s boring if you’ve seen the JSON Feed format description, but a couple of things to point out:

  1. We’re programmatically grabbing the JSON Feed permalink, rather than hard-coding it. If you have multiple feeds on your site (e.g., one per category), that’ll help things work out
  2. The {{ range $index, $entry := ... }} silliness is the only way in Go templates to handle fence posts. In this case, because JSON does not allow trailing commas, we need to prevent having an extra comma at the end, and the easiest way to do that is to inject a comma before every entry except the first. Caching the $index lets us easily do that (and taking advantage of 0 being falsy in Go templates makes the conditional short, too).
  3. Finally, the hyphens on some of the {{ ... }} injections deletes preceding (if it’s directly after the opening brace) and trailing (if it’s directly before the close brace) whitespace, which mostly isn’t programmatically necessary here, but keeps the JSON looking clean.

Add the <link> to your index page

The last step is to tell the world about your new feed. On your main index page, just add

<link
  href="{{ with .OutputFormats.Get "jsonfeed"  }}{{ .Permalink }}{{ end }}"
  rel="alternate" type="application/json" title="{{ .Site.Title }}" />

There shouldn’t be anything surprising there. We’re reusing the {{ with .OutputFormats.Get ... }} trick from earlier to avoid hard-coding the feed URL, and the rest is straightforward templating.

So there you have it: that’s all it takes to add JSON Feed to your Hugo blog. I look forward to the next entry, in which we can explore how to add YAML Feed, EDN Feed, and maybe some custom Microsoft-specific extensions to both of those as well.


  1. No one tell them what HTML is. I really do not want to see JHTML. At least, not more so than I already have it with React. ↩︎

  2. "index" would’ve been another fine choice, and in line with other Hugo templates; I just found "feed" clearer. ↩︎