Eclectic Media Git / 9503086
Draft 1 final revision Ariana Giroux 29 days ago
1 changed file(s) with 131 addition(s) and 58 deletion(s). Raw diff Collapse all Expand all
8888 Put it all together and add a few extra pieces of meta data, and we end up with a fully serviceable (and extendible) RSS feed file
90 <?xml version="1.0" encoding="utf-8"?>
91 <feed xmlns="">
93 <title>{{ site_title }}</title>
94 <link href="{{ site_link }}"/>
95 <updated>{{ site_last_updated }}</updated> <!--datetime stamp for last updated-->
96 <author>
97 {%- for author in site_authors %}
98 <name>{{ author }}</name>
99 {%- endfor %}
100 </author>
101 <id>{{ site_id }}</id>
103 {%- for item in items %}
104 <entry>
105 <title>{{ item['title'] }}</title>
106 <link href="{{ item['link'] }}"/>
107 <id>{{ item['id'] }}</id>
108 <updated>{{ item['last-updated'] }}</updated>
109 <summary>{{ item['summary'] }}</summary>
110 {%- if item['category'] | len > 0 %}<category>{{ item['category'] }}</category>{% endif %}
111 </entry>
112 {%- endfor %}
114 </feed>
90 <?xml version="1.0" encoding="UTF-8" ?>
91 <rss version="2.0" xmlns:atom="">
93 <channel>
94 <title>{{ title }}</title>
95 <link>{{ link }}</link>
96 <description>{{ description }}</description>
97 <atom:link href="" rel="self" type="application/rss+xml" />
98 {%- for item in items %}
99 <item>
100 <title>{{ item['title'] }}</title>
101 <link>{{ item['link'] }}</link>
102 <description>{{ item['description'] }}</description>
103 <guid>{{ item['link'] }}</guid>
104 </item>
105 {%- endfor %}
106 </channel>
108 </rss>
116110 > A quick aside, the category tag is inside some simple Jinja2 logic to determine if a category was included.
138132 > <small>Follow <a href="">this link</a> to view the code in full.</small>
140 ### Extending the module
142 One of the first pieces of data we need to include in our feed template is the feed "last updated time." The RSS standard requires that the feed include the last time content is updated so that an end-user can know if they need to pull more data or not. We're already checking the `last modified time` of blog content (twice in fact), so maintaining a `most recently modified time` only requires we perform a greater than delta check on each iteration of `sort_blog()`.
144 First, the code as it is now (as it is in [433f520](
146 def sort_blog():
147 for name, link, path in __process_blog_names():
148 yield name, link, path, os.path.getmtime(path), __get_blog_text(path)
150 > The call to `os.path.getmtime` gives us the last modified time of the current file in the list.
152 Because `getmtime` returns [UNIX time](, we are able to simply use a greater than delta check to determine if we need to update the last modified time. For example:
154 <pre class="f9 b9"><code> def sort_blog():
155 <span class="f2">+ last_updated = 0</span>
156 <span class="f2">+</span>
157 for name, link, path in __process_blog_names():
158 <span class="f1">- yield name, link, path, os.path.getmtime(path), __get_blog_text(path)</span>
159 <span class="f2">+ mtime = os.path.getmtime(path)</span>
160 <span class="f2">+ last_updated = mtime if mtime &gt; last_updated else last_updated</span>
161 <span class="f2">+ yield name, link, path, mtime, __get_blog_text(path)</span></code>
162 </pre>
165 Now that we have all of the information we need, we can get to setting up our endpoint!
167 > Note: we haven't currently added a mechanism for adding category or contributor information to the articles. This will be handled in a future update.
170 ---
172 ## Putting it All Together
134 ### Using the module to populate our feed
136 Because `sort_blog()` gives us all the data we need for our `RSS` template feed, all we 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 our data object.
138 > Note: Jinja2 holds the *entire* context for its template within a dict, and we will be overriding it with ours. Therefore, we will end up with a `dictionary` that only contains a list of items.
140 First, lets collect that data:
142 context = {'items': []} # initialize our context variable
144 for item in sort_blog(): # iterate through each blog article, yielding relavent data
146 context['items'].append( # Add the following dictionary to the items list
147 {
148 'title': item[0], # set the title
149 'link': '{}'.format(item[1]), # set the article URI
150 'description': item[-1], # set the description using utilities.__get_blog_text
151 }
152 )
154 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.
156 Due to this, we need to write a new description parser for the rss feed. Because we need plain-text and our `markdown` files are human readable, we can simply grab the first body line from the file:
159 def parse_description(path):
160 with open(path) as f:
161 text = # obtain file text
163 for line in text.split('\n'): # split file by line
165 if len(line) > 0 and line[0] != '#': # ensure line has content, and isnt a header line
167 return line.strip('\n') # exit loop on first line that matches
169 So let's replace that problem line:
171 ...
172 context['items'].append( # Add the following dictionary to the items list
173 {
174 'title': item[0], # set the title
175 'link': '{}'.format(item[1]), # set the article URI
176 'description': parse_description(item[2]), # use the path returned by sort_blog instead
177 }
178 )
179 ...
181 With all of this in place, we can use Jinja2 and our [blog discovery module]( to render a fully valid RSS feed file! Now let's patch it in to a new endpoint.
183 ## Setting up the RSS endpoint
185 All that is left to do now is to add a new endpoint to the [flask]( app-file (*`./`*). To do so, we'll first set up some scaffolding with flask:
188 @app.route('/rss/feed.xml', methods=['GET'])
189 def feed():
190 return 'our feed!'
193 Once we have this function added to our app-file, we can start plugging in the code from the previous step. First, the context setup:
195 @app.route('/rss/feed.xml', methods=['GET'])
196 def feed():
198 context = {'items': []}
199 for item in sort_blog():
200 context['items'].append(
201 {
202 'title': item[0],
203 'link': '{}'.format(item[1]),
204 'description': parse_description(item[2]),
205 }
206 )
208 context['title'] = 'Eclectic Media Solutions Blog'
209 context['link'] = ''
210 context['description'] = 'Open source tech musings'
212 return 'our feed!'
214 And now we can plug in that parsing function. Instead of adding it to the general namespace 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 maintenence in the future.
216 @app.route('/rss/feed.xml', methods=['GET'])
217 def feed():
218 def parse_description(path):
219 with open(path) as f:
220 text =
222 for line in text.split('\n'):
223 if len(line) > 0 and line[0] != '#':
224 return line.strip('\n')
226 context = {'items': []}
227 ...
228 return render('feed.xml', context=context) # use our rendering function to return the feed file
230 Experienced Flask developers may have already noticed my error. In the above snippet, I added a call to the [jinja renderer]( and thought it would work out in my favor. I should know by know 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.
232 @app.route('/rss/feed.xml', methods=['GET'])
233 def feed():
234 ...
235 return Response(render('feed.xml', context=context),
236 mimetype='text/xml')
239 Now that we'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.
241 ## Wrapping it all up
243 In conclusion, 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 our own `RSS` feed has allowed me to not only take a step closer to achieving those goals, but also to forever extend the funcitonality of the webiste in a simple and efficient matter. With the feed in place, maintenence of the site has become easier without sacrificing the potential of returning visitors.
245 Make sure to subscribe to the feed at []( to get any updates!