Ensuring Less Notifications

It is no secret that notifications are the leading cause for pulling us away from our work. According to Business of Apps, "the average user receives 46 app push notifications per day", and "40% of web push notification senders belong to either the e-commerce or media, publishing and blogging sectors." In today’s world we are increasingly being pulled away from reality by our phones. The distractions caused by notifications lower our productivity, and as such I wish not to contribute to the growing amount of notification based distractions.

Many sources have shown that multitasking reduces our productivity. As this research from the APA shows, "[...] multitasking may seem efficient on the surface but may actually take more time in the end and involve more error." With our phones and computers becoming more and more prominent in our lives, the notifications they bring become more and more regular distractions from our work. In short, each time you check your phone you are polluting what cognitive processing you have.

Enter RSS/Atom

For the uninitiated, RSS is a long standing web standard that has a wonderfully simple name: Really Simple Syndication

I know, I know, that isn't really the technical name... Leave me alone.

RSS allows low bandwidth update and content syndication in a method that allows a user to choose for themselves how those notifications are consumed. Many apps are available to collect RSS feeds and provide "push" notifications for your device. Due to the purely opt in design of the standard, it puts the power in the user's hands without forcing them to trade their time and concentration (or their data privacy).

Using RSS/Atom instead of a more modern solution allows not only stability, but also user freedom. Due to the design of RSS/Atom, a user's device does not inherently need to maintain a regular or constant connection to the server, instead only fetching updates when the user decides. CommaFeed (an open source and self hosted feed reader app) showcases the effectiveness of RSS/Atom very well. CommaFeed can operate by only searching for new feed content on login, instead of continuously in the background. This can help prevent contribution to the aforementioned 46 notifications an hour by allowing the user to dictate their own reading habits, instead of letting the site dictate the user's habits.

The Implementation

Implementing an RSS/Atom feed for the website is not a difficult process. Currently, the syndication needs for the blog/site are simple, "when an article is published, you should know about it". Due to the blog article discovery module (a simple file discovery and markdown rendering module), patching in an RSS/Atom feed only requires some simple extension.

So here's our patch outline:

  1. Create an RSS/Atom template with Jinja2

  2. Use the aforementioned module to yield data for that RSS/Atom template

  3. Create an endpoint to handle /blog/feed GET requests that fetches data from step #2 and uses the template from step #1 to render and return that data

Creating an RSS Template


Due to the standard's simplicity, setting up a basic template that can produce the entire feed should be a relatively easy process. We can obtain a full "example" file, and apply some basic Jinja2 for loops.

First, the sample file mentioned above:

    <?xml version="1.0" encoding="UTF-8" ?>
    <rss version="2.0">

    <channel>
          <title>W3Schools Home Page</title> <!--site title-->
          <link>https://www.w3schools.com</link> <!--site base link-->
          <description>Free web building tutorials</description> <!--feed description-->

            <!--These next two item elements can be repeated with a for loop-->
          <item> <!--a single entry in the feed-->
              <title>RSS Tutorial</title>
              <link>https://www.w3schools.com/xml/xml_rss.asp</link>
              <description>New RSS tutorial on W3Schools</description>
          </item>

          <item> <!--a single entry in the feed-->
              <title>XML Tutorial</title>
              <link>https://www.w3schools.com/xml</link>
              <description>New XML tutorial on W3Schools</description>
          </item>
    </channel>

    </rss>

As is pointed out in my added comments in the above code, I can almost instantly reduce the amount of handwritten XML by adding a for loop to the <item> keys:

    {% for item in items %} {# assuming items is a dict containing relavent data #}
    <item> <!--a single entry in the feed-->
        <title>{{ item['title'] }}</title>
        <link>{{ item['link'] }}</link>
        <description>{{ item['description'] }}</description>
    </item>
    {% endfor %}

For those not familiar with Jinja2, {% for ... %} simply tells the template to repeat the code within the beginning and end tag for each data point in a list. Furthermore, the {{ item['key'] }} is Python syntax for accessing dictionary data.

We're now able to avoid any potential errors with hand crafting each item in the feed! The only thing thats left for the template is to add Jinja2 data where necessary:

    <?xml version="1.0" encoding="UTF-8" ?>
    <rss version="2.0">

    <channel>
      <title>{{ title }}</title> <!--site title-->
      <link>{{ link }}</link> <!--site base link-->
      <description>{{ description }}</description> <!--feed description-->
      <!--... item tags ...->
    </channel>

    </rss>

Put it all together and add a few extra pieces of meta data, and I end up with a fully serviceable (and extendible) RSS feed file

<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">

<channel>
  <title>{{ title }}</title>
  <link>{{ link }}</link>
  <description>{{ description }}</description>
  <atom:link href="https://eclecticmedia.space/rss/feed.xml" rel="self" type="application/rss+xml" />
  {%- for item in items %}
  <item>
    <title>{{ item['title'] }}</title>
    <link>{{ item['link'] }}</link>
    <description>{{ item['description'] }}</description>
    <guid>{{ item['link'] }}</guid>
  </item>
  {%- endfor %}
</channel>

</rss>

A quick aside, the category tag is inside some simple Jinja2 logic to determine if a category was included.


Now that I have a template file set up, I can use it to render the feed perfectly up to date (to the moment the request for content is made) with the current state of the site. This allows me to provide a simple content upload mechanism (like the currently used git system), and keep the feed up to date without worrying about potentially blocking database queries. Each time an end-user RSS reader attempts to get the feed, the site can populate the above template with details mostly already present in the discovery engine.

Using the Discovery Module to obtain additional data for the RSS feed


Discovery Module Walk Through

The entire module consists of 4 functions, where 3 are primary functions, and one being a helper function. First, the primary functions:

  1. __yield_files(): Yields files available in the /content/blog directory, sorted by last modified time.

  2. __process_blog_names(): Converts /content/blog/file-name.md to valid URI endpoints (for example, "/blog/file_name").

  3. sort_blog(): Yields the processed blog name for the template, a valid URI, the path to the file, the last modified time, and the article flavour text.

Note, sort_blog() uses the helper function __get_blog_text() to yield the first paragraph from an article for flavour text.

Follow this link to view the code in full.

Using the module to populate the feed

Because sort_blog() yields all the data the function needs for the RSS template feed, all I need to do is collect its' data in a context variable and pass it on to the template. Jinja2 has an excellent method to interact with data in python dictonaries, so that will be the data object.

Note: Jinja2 holds the entire context for its template within a dict, and I will be overriding it with ours. Therefore, the function will end up with a dictionary that only contains a list of items.

First, lets collect that data:

context = {'items': []}  # initialize our context variable

for item in sort_blog():  # iterate through each blog article, yielding relavent data

    context['items'].append(  # Add the following dictionary to the items list
        {
            'title': item[0],  # set the title
            'link': 'https://eclecticmedia.space/blog/{}'.format(item[1]),  # set the article URI
            'description': item[-1],  # set the description using utilities.__get_blog_text
        }
    )

This is where I ran into my first problem however. The description text that is returned by sort_blog() (and therefore __get_blog_text()) is the HTML that has been rendered from the plain-text markdown. HTML and XML syntax are far too similar to have this be a safe option, and will often end up in syntax errors and the feed failing to render.

Due to this, I need to write a new description parser for the RSS feed. Because the function needs plain-text and the markdown files are human readable, I can simply grab the first body line from the file:

def parse_description(path):
    with open(path) as f:
        text = f.read()  # obtain file text

    for line in text.split('\n'):  # split file by line

        if len(line) > 0 and line[0] != '#':  # ensure line has content, and isnt a header line

            return line.strip('\n')  # exit loop on first line that matches

So let's replace that problem line:

    ...
    context['items'].append(  # Add the following dictionary to the items list
        {
            'title': item[0],  # set the title
            'link': 'https://eclecticmedia.space/blog/{}'.format(item[1]),  # set the article URI
            'description': parse_description(item[2]),  # use the path returned by sort_blog instead
        }
    )
    ...

With all of this in place, I can use Jinja2 and my blog discovery module to render a fully valid RSS feed file! Now let's patch it in to a new endpoint.

Setting up the RSS endpoint

All that is left to do now is to add a new endpoint to the flask app-file (./app.py). To do so, I'll first set up some scaffolding with flask:

@app.route('/rss/feed.xml', methods=['GET'])
def feed():
    return 'our feed!'

Once this function has been added to the app-file, I can start plugging in the code from the previous step. First, the context setup:

@app.route('/rss/feed.xml', methods=['GET'])
def feed():

    context = {'items': []}
    for item in sort_blog():
        context['items'].append(
            {
                'title': item[0],
                'link': 'https://eclecticmedia.space/blog/{}'.format(item[1]),
                'description': parse_description(item[2]),
            }
        )

    context['title'] = 'Eclectic Media Solutions Blog'
    context['link'] = 'https://eclecticmedia.space/blog/'
    context['description'] = 'Open source tech musings'

    return 'our feed!'

Now I can plug in that parsing function. Instead of adding it to the general name space and scope of the app-file, it is probably more ideal to simply declare it in the endpoint function itself (a lesser known feature of python). This allows the code to stay leaner, and allows easier maintenance in the future.

@app.route('/rss/feed.xml', methods=['GET'])
def feed():
    def parse_description(path):
        with open(path) as f:
            text = f.read()

        for line in text.split('\n'):
            if len(line) > 0 and line[0] != '#':
                return line.strip('\n')

    context = {'items': []}
    ...
    return render('feed.xml', context=context)  # use our rendering function to return the feed file

Experienced Flask developers may have already noticed my error. In the above snippet, I added a call to the Jinja2 renderer and thought it would work out in my favor. I should know by now that it's never as simple as just popping in the previous step with code. Turns out I needed to add a Flask class to set up the response as XML instead of HTML as flask is want to do.

@app.route('/rss/feed.xml', methods=['GET'])
def feed():
  ...
  return Response(render('feed.xml', context=context),
                  mimetype='text/xml')

Now that I've told Flask to return an XML file instead of an HTML response, my feed is set up and ready for syndication! Now, any time I update or publish an article, feed readers around the world can notify users of those changes according to their own rules.

Wrapping it all up

Through my work I have shown that it is in keeping with my goals for this site to offer lightweight, privacy respecting, and flexible systems and solutions to my readers and users. Rolling my own RSS feed has allowed me to not only take a step closer to achieving those goals, but also to forever extend the functionality of the website in a simple and efficient matter. With the feed in place, maintenance of the site has become easier without sacrificing the potential of returning visitors.

Make sure to subscribe to the feed at https://eclecticmedia.space/rss/feed.xml to get any updates!

Back to the Blog Index