Eclectic Media Git eclecticmedia.space / 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
8787
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
8989
90 <?xml version="1.0" encoding="utf-8"?>
91 <feed xmlns="http://www.w3.org/2005/Atom">
92
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>
102
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 %}
113
114 </feed>
90 <?xml version="1.0" encoding="UTF-8" ?>
91 <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
92
93 <channel>
94 <title>{{ title }}</title>
95 <link>{{ link }}</link>
96 <description>{{ description }}</description>
97 <atom:link href="https://eclecticmedia.space/rss/feed.xml" 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>
107
108 </rss>
115109
116110 > A quick aside, the category tag is inside some simple Jinja2 logic to determine if a category was included.
117111
137131
138132 > <small>Follow <a href="https://git.eclecticmedia.space/public/eclecticmedia.space/blob/899e28c6a0efae47f85c4d04b297b8cadac25293/utilities/__init__.py">this link</a> to view the code in full.</small>
139133
140 ### Extending the module
141
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()`.
143
144 First, the code as it is now (as it is in [433f520](https://git.eclecticmedia.space/public/eclecticmedia.space/commit/433f520731ee69dfdc205b1797b056ba2ce337bf/)):
145
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)
149
150 > The call to `os.path.getmtime` gives us the last modified time of the current file in the list.
151
152 Because `getmtime` returns [UNIX time](https://en.wikipedia.org/wiki/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:
153
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>
163
164
165 Now that we have all of the information we need, we can get to setting up our endpoint!
166
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.
168
169
170 ---
171
172 ## Putting it All Together
134 ### Using the module to populate our feed
135
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.
137
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.
139
140 First, lets collect that data:
141
142 context = {'items': []} # initialize our context variable
143
144 for item in sort_blog(): # iterate through each blog article, yielding relavent data
145
146 context['items'].append( # Add the following dictionary to the items list
147 {
148 'title': item[0], # set the title
149 'link': 'https://eclecticmedia.space/blog/{}'.format(item[1]), # set the article URI
150 'description': item[-1], # set the description using utilities.__get_blog_text
151 }
152 )
153
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.
155
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:
157
158
159 def parse_description(path):
160 with open(path) as f:
161 text = f.read() # obtain file text
162
163 for line in text.split('\n'): # split file by line
164
165 if len(line) > 0 and line[0] != '#': # ensure line has content, and isnt a header line
166
167 return line.strip('\n') # exit loop on first line that matches
168
169 So let's replace that problem line:
170
171 ...
172 context['items'].append( # Add the following dictionary to the items list
173 {
174 'title': item[0], # set the title
175 'link': 'https://eclecticmedia.space/blog/{}'.format(item[1]), # set the article URI
176 'description': parse_description(item[2]), # use the path returned by sort_blog instead
177 }
178 )
179 ...
180
181 With all of this in place, we can use Jinja2 and our [blog discovery module](https://git.eclecticmedia.space/public/eclecticmedia.space/blob/899e28c6a0efae47f85c4d04b297b8cadac25293/utilities/__init__.py) to render a fully valid RSS feed file! Now let's patch it in to a new endpoint.
182
183 ## Setting up the RSS endpoint
184
185 All that is left to do now is to add a new endpoint to the [flask](https://palletsprojects.com/p/flask/) app-file (*`./app.py`*). To do so, we'll first set up some scaffolding with flask:
186
187
188 @app.route('/rss/feed.xml', methods=['GET'])
189 def feed():
190 return 'our feed!'
191
192
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:
194
195 @app.route('/rss/feed.xml', methods=['GET'])
196 def feed():
197
198 context = {'items': []}
199 for item in sort_blog():
200 context['items'].append(
201 {
202 'title': item[0],
203 'link': 'https://eclecticmedia.space/blog/{}'.format(item[1]),
204 'description': parse_description(item[2]),
205 }
206 )
207
208 context['title'] = 'Eclectic Media Solutions Blog'
209 context['link'] = 'https://eclecticmedia.space/blog/'
210 context['description'] = 'Open source tech musings'
211
212 return 'our feed!'
213
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.
215
216 @app.route('/rss/feed.xml', methods=['GET'])
217 def feed():
218 def parse_description(path):
219 with open(path) as f:
220 text = f.read()
221
222 for line in text.split('\n'):
223 if len(line) > 0 and line[0] != '#':
224 return line.strip('\n')
225
226 context = {'items': []}
227 ...
228 return render('feed.xml', context=context) # use our rendering function to return the feed file
229
230 Experienced Flask developers may have already noticed my error. In the above snippet, I added a call to the [jinja renderer](https://git.eclecticmedia.space/public/eclecticmedia.space/blob/master/utilities/renderer.py#L-10) 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.
231
232 @app.route('/rss/feed.xml', methods=['GET'])
233 def feed():
234 ...
235 return Response(render('feed.xml', context=context),
236 mimetype='text/xml')
237
238
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.
240
241 ## Wrapping it all up
242
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.
244
245 Make sure to subscribe to the feed at [https://eclecticmedia.space/rss/feed.xml](https://eclecticmedia.space/rss/feed.xml) to get any updates!