JSON Feed in Pelican

Posted on 2017-05-18

Brent Simmons and Manton Reece recently announced an alternative to RSS and Atom using JSON. The format is straight forward and seemed like a great fit to implement in Pelican.

I've been spending a considerable amount of my time lately writing Apex code (Salesforce's proprietary language similar to Java and C#) and have come to appreciate it's ability to serialize different objects. Python isn't particularly good at this, and so I initially struggled with coming up with a clean way of implementing the generator. The new JSON feed spec has many nested objects and so representing these as separate classes made sense. Let's look at an author

class Author(Object):
    def __init__(self, name, url=None, avatar=None):
        self.name = name
        self.url = url
        self.avatar = avatar

This is a basic representation of an author based on JSON feed's spec. If we were to simply try to serialize this in Python using the json library, we'd come across this exception

TypeError: <__main__.Author object at 0x107d23e10> is not JSON serializable

The json library allows you to pass in your own custom parser, and so I created a base class for all my my objects that would contain one method that the parser would look for as a way to tell it how to serialize each class.

import json

class JSONEncoder(json.JSONEncoder):
    def default(self, obj):
        if hasattr(obj, 'as_json'):
            return obj.as_json()
        else:
            return json.JSONEncoder.default(self, obj)

class Base(object):
    def as_json(self):
        return self.__dict__

class Author(Base):
    def __init__(self, name, url=None, avatar=None):
        self.name = name
        self.url = url
        self.avatar = avatar

Now we can call the same method, while passing in our custom JSON encoder to serialize our class

a = Author('Ryan M')
json.dumps(a, cls=JSONEncoder)
# '{"url": null, "name": "Ryan M", "avatar": null}'

Now it's just a matter of building a class for each object in the JSON feed top-level object

class Item(Base):
    pass

# a list of Item classes, since there are many
class Items(list):
      # list type doesn't have a __dict__ accessor, so we just return the list to be serialized
    def as_json(self):
        return self

# The top-level JSON feed object containing all child objects
class JsonFeed(Base):
    pass

I've left out the implementation details for generating each of these objects for brevity, but the idea is all there. Each class now knows how to tell the json encoder how to be serialized, so it's just a matter of implementing the Pelican plugin and writing the output.

class JsonFeedGenerator(object):
    def __init__(self, article_generator):
        self.articles = article_generator.articles
        self.settings = article_generator.settings
        self.context = article_generator.context
        self.generator = article_generator

        self.path = 'feed.json'

        self.site_url = article_generator.context.get('SITEURL',
                                                      path_to_url(get_relative_path(self.path)))

        self.feed_domain = self.context.get('FEED_DOMAIN')
        self.feed_url = '{}/{}'.format(self.feed_domain, self.path)

    def write_feed(self):
        complete_path = os.path.join(self.generator.output_path, self.path)
        try:
            os.makedirs(os.path.dirname(complete_path))
        except Exception:
            pass

        with open(complete_path, 'w') as f:
            json.dump(JsonFeed.from_generator(self), f, cls=JSONEncoder)

def get_generators(article_generator):
    json_feed_generator = JsonFeedGenerator(article_generator)
    json_feed_generator.write_feed()

def register():
    signals.article_generator_finalized.connect(get_generators)

You can see the feed for this blog here. The source for the entire plugin can be found on Github here. The plugin should work for all sites right now. I chose not to implement multiple languages into the feed since it doesn't seem like the spec supports this. Hopefully they consider this as they improve the format.

Tags: pelican python

json-feed-in-pelican

© Ryan M 2023. Built using Pelican.