wezm.net/v2/content/posts/2024/json-feed-zola.md

3.3 KiB

+++ title = "Generate a JSON Feed for a Zola Website" date = 2024-09-28T20:50:30+10:00

[extra] updated = 2024-09-30T09:08:57+10:00 +++

JSON Feed is a specification for representing an RSS-style feed in JSON. I wanted to add one as an alternative alongside the Atom feed on a new website I'm building. The website is built with Zola, which unfortunately doesn't support the format, so this is how I went about adding one.

My first idea was to add a Tera template json.feed and try to generate JSON in the template. This was foiled on multiple fronts:

  1. Zola only loads files matching *.{*ml,md} and robots.txt as templates, so it didn't render my json.feed template.
  2. Tera has a json_encode filter but does not support object literals, which makes building well formed JSON tricky.

My next thought was to write a tool to convert the Atom feed to JSON. Rust is my scripting language of choice these days (only a tiny bit joking) but I didn't feel like adding a Rust project and build step for this. I then wondered if it could be done with jq, or in my case jaq—a jq implementation in Rust. Short answer it can. Here's what I came up with:

I created templates/dump.xml with these contents:

{{ __tera_context }}

This dumps the whole Tera context as JSON when rendered into public/dump.xml. It's .xml so that Zola will render it as a template. I added it to feed_filenames in the Zola config.toml:

feed_filenames = [
  "atom.xml",
  "dump.xml", # HACK: This just dumps the Tera context as JSON
]

Then I wrote this jaq filter (json-feed.jaq) to generate a JSON Feed feed.json from the rendered version of the template.

# vim: ft=jq
# Generate a JSON feed from context of a Zola feed template
{
    "version": "https://jsonfeed.org/version/1.1",
    "title": .config.title,
    "home_page_url": .config.base_url,
    "feed_url": .feed_url | sub("dump\\.xml$"; "feed.json"),
    "authors": [ { "name": .config.author } ],
    "language": "en-AU",
    "items": .pages | map({
            "id": .permalink,
            "url": .permalink,
            "title": .title,
            "content_html": .content,
            "date_published": .date,
            "tags": .taxonomies.tags,
    } + if .updated then {"date_modified": .updated} else {} end
      + if .extra.link then {"external_url": .extra.link} else {} end)
}

It turned out quite nice. The main challenge with the jaq filter was working out how to conditionally include a key only if the value was non-null. I ended up with the trailing if-then-else-end expressions. If there's a better way I'd be keen to hear about it.

Finally, to coordinate all this I added a Makefile that deletes dump.xml afterwards:

help:
	@echo "Available tasks:"
	@echo
	@echo "- build"
	@echo "- deploy"

build:
	zola build
	jaq --from-file json-feed.jaq public/dump.xml > public/feed.json
	rm public/dump.xml

deploy:
	@echo todo

.PHONY: build depoly