Eclectic Media Git archived_em.space_site / 8c352b0
Merge branch 'rss-draft-1' Ariana Giroux 8 months ago
2 changed file(s) with 865 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 ## Ensuring Less Notifications
1
2
3 It is no secret that notifications are the leading cause for pulling us away from our work. According to [Business of Apps](https://www.businessofapps.com/marketplace/push-notifications/research/push-notifications-s), "*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.
4
5 Many sources have shown that [multitasking reduces our productivity](https://www.apa.org/research/action/multitask). As this [research from the APA](https://www.apa.org/research/action/multitask) 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.
6
7 ## Enter RSS/Atom
8
9 For the uninitiated, RSS is a long standing [web standard](https://validator.w3.org/feed/docs/rss2.html) that has a wonderfully *simple* name: `Really Simple Syndication`
10
11 > <small><em>I know, I know, that isn't really the technical name... Leave me alone.</small></em>
12
13 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*).
14
15 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](https://www.commafeed.com/#/welcome) (*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.
16
17 ## The Implementation
18
19 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](https://git.eclecticmedia.space/public/eclecticmedia.space/blob/899e28c6a0efae47f85c4d04b297b8cadac25293/utilities/__init__.py) (*a simple file discovery and markdown rendering module*), patching in an RSS/Atom feed only requires some simple extension.
20
21 So here's our patch outline:
22
23 1. Create an RSS/Atom template with `Jinja2`
24
25 2. Use [the aforementioned module](https://git.eclecticmedia.space/public/eclecticmedia.space/blob/899e28c6a0efae47f85c4d04b297b8cadac25293/utilities/__init__.py) to yield data for that RSS/Atom template
26
27 4. 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
28
29 <h2 style="margin-top: 2rem;">Creating an RSS Template</h2>
30
31 ---
32
33 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](https://www.w3schools.com/xml/xml_rss.asp)" file, and apply some basic ``Jinja2 for loops``.
34
35 First, the [sample file](https://www.w3schools.com/xml/xml_rss.asp) mentioned above:
36
37 <?xml version="1.0" encoding="UTF-8" ?>
38 <rss version="2.0">
39
40 <channel>
41 <title>W3Schools Home Page</title> <!--site title-->
42 <link>https://www.w3schools.com</link> <!--site base link-->
43 <description>Free web building tutorials</description> <!--feed description-->
44
45 <!--These next two item elements can be repeated with a for loop-->
46 <item> <!--a single entry in the feed-->
47 <title>RSS Tutorial</title>
48 <link>https://www.w3schools.com/xml/xml_rss.asp</link>
49 <description>New RSS tutorial on W3Schools</description>
50 </item>
51
52 <item> <!--a single entry in the feed-->
53 <title>XML Tutorial</title>
54 <link>https://www.w3schools.com/xml</link>
55 <description>New XML tutorial on W3Schools</description>
56 </item>
57 </channel>
58
59 </rss>
60
61 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:
62
63 {% for item in items %} {# assuming items is a dict containing relavent data #}
64 <item> <!--a single entry in the feed-->
65 <title>{{ item['title'] }}</title>
66 <link>{{ item['link'] }}</link>
67 <description>{{ item['description'] }}</description>
68 </item>
69 {% endfor %}
70
71 > 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.
72 > Furthermore, the `{{ item['key'] }}` is Python syntax for accessing dictionary data.
73
74 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:
75
76 <?xml version="1.0" encoding="UTF-8" ?>
77 <rss version="2.0">
78
79 <channel>
80 <title>{{ title }}</title> <!--site title-->
81 <link>{{ link }}</link> <!--site base link-->
82 <description>{{ description }}</description> <!--feed description-->
83 <!--... item tags ...->
84 </channel>
85
86 </rss>
87
88 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
89
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>
109
110 > A quick aside, the category tag is inside some simple Jinja2 logic to determine if a category was included.
111
112 ---
113
114 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.
115
116 <h2 style="margin-top: 2rem;">Using the Discovery Module to obtain additional data for the RSS feed</h2>
117
118 ---
119
120 ### Discovery Module Walk Through
121
122 The entire module consists of 4 functions, where 3 are primary functions, and one being a [helper function](https://git.eclecticmedia.space/public/eclecticmedia.space/blob/899e28c6a0efae47f85c4d04b297b8cadac25293/utilities/__init__.py#L-23). First, the primary functions:
123
124 1. `__yield_files()`: *Yields files available in the `/content/blog` directory, sorted by last modified time.*
125
126 2. `__process_blog_names()`: *Converts `/content/blog/file-name.md` to valid URI endpoints (for example, "`/blog/file_name`").*
127
128 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.*
129
130 > <small>Note, `sort_blog()` uses the helper function `__get_blog_text()` to yield the first paragraph from an article for flavour text.</small>
131
132 > <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>
133
134 ### Using the module to populate the feed
135
136 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.
137
138 > 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.
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, 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:
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, I can use Jinja2 and my [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, I'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 this function has been added to the app-file, I 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 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.
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 [Jinja2 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 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.
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 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.
240
241 ## Wrapping it all up
242
243 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.
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!
0 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
1 <!-- Created with Inkscape (http://www.inkscape.org/) -->
2
3 <svg
4 xmlns:dc="http://purl.org/dc/elements/1.1/"
5 xmlns:cc="http://creativecommons.org/ns#"
6 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
7 xmlns:svg="http://www.w3.org/2000/svg"
8 xmlns="http://www.w3.org/2000/svg"
9 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
10 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
11 width="4in"
12 height="7in"
13 viewBox="0 0 101.6 177.8"
14 version="1.1"
15 id="svg8"
16 inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
17 sodipodi:docname="899e28c-discovery-engine.svg">
18 <defs
19 id="defs2">
20 <marker
21 inkscape:isstock="true"
22 style="overflow:visible"
23 id="marker6344"
24 refX="0"
25 refY="0"
26 orient="auto"
27 inkscape:stockid="DiamondL">
28 <path
29 transform="scale(0.8)"
30 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
31 d="M 0,-7.0710768 -7.0710894,0 0,7.0710589 7.0710462,0 Z"
32 id="path6342"
33 inkscape:connector-curvature="0" />
34 </marker>
35 <marker
36 inkscape:isstock="true"
37 style="overflow:visible"
38 id="marker6232"
39 refX="0"
40 refY="0"
41 orient="auto"
42 inkscape:stockid="TriangleInL">
43 <path
44 transform="scale(-0.8)"
45 style="fill:#8900ad;fill-opacity:1;fill-rule:evenodd;stroke:#8900ad;stroke-width:1.00000003pt;stroke-opacity:1"
46 d="M 5.77,0 -2.88,5 V -5 Z"
47 id="path6230"
48 inkscape:connector-curvature="0" />
49 </marker>
50 <marker
51 inkscape:isstock="true"
52 style="overflow:visible"
53 id="marker5698"
54 refX="0"
55 refY="0"
56 orient="auto"
57 inkscape:stockid="TriangleInL">
58 <path
59 transform="scale(-0.8)"
60 style="fill:#8900ad;fill-opacity:1;fill-rule:evenodd;stroke:#8900ad;stroke-width:1.00000003pt;stroke-opacity:1"
61 d="M 5.77,0 -2.88,5 V -5 Z"
62 id="path5696"
63 inkscape:connector-curvature="0" />
64 </marker>
65 <marker
66 inkscape:stockid="Arrow2Lstart"
67 orient="auto"
68 refY="0"
69 refX="0"
70 id="marker3736"
71 style="overflow:visible"
72 inkscape:isstock="true">
73 <path
74 id="path3734"
75 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
76 d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
77 transform="matrix(1.1,0,0,1.1,1.1,0)"
78 inkscape:connector-curvature="0" />
79 </marker>
80 <marker
81 inkscape:stockid="Arrow2Lstart"
82 orient="auto"
83 refY="0"
84 refX="0"
85 id="marker3630"
86 style="overflow:visible"
87 inkscape:isstock="true">
88 <path
89 id="path3628"
90 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
91 d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
92 transform="matrix(1.1,0,0,1.1,1.1,0)"
93 inkscape:connector-curvature="0" />
94 </marker>
95 <marker
96 inkscape:isstock="true"
97 style="overflow:visible"
98 id="marker3512"
99 refX="0"
100 refY="0"
101 orient="auto"
102 inkscape:stockid="Arrow2Lstart">
103 <path
104 transform="matrix(1.1,0,0,1.1,1.1,0)"
105 d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
106 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
107 id="path3510"
108 inkscape:connector-curvature="0" />
109 </marker>
110 <marker
111 inkscape:isstock="true"
112 style="overflow:visible"
113 id="marker3312"
114 refX="0"
115 refY="0"
116 orient="auto"
117 inkscape:stockid="Arrow2Lstart">
118 <path
119 transform="matrix(1.1,0,0,1.1,1.1,0)"
120 d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
121 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
122 id="path3310"
123 inkscape:connector-curvature="0" />
124 </marker>
125 <marker
126 inkscape:isstock="true"
127 style="overflow:visible"
128 id="marker3302"
129 refX="0"
130 refY="0"
131 orient="auto"
132 inkscape:stockid="Arrow2Lstart">
133 <path
134 transform="matrix(1.1,0,0,1.1,1.1,0)"
135 d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
136 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
137 id="path3300"
138 inkscape:connector-curvature="0" />
139 </marker>
140 <marker
141 inkscape:isstock="true"
142 style="overflow:visible"
143 id="marker3062"
144 refX="0"
145 refY="0"
146 orient="auto"
147 inkscape:stockid="DiamondL">
148 <path
149 transform="scale(0.8)"
150 style="fill:#8900ad;fill-opacity:1;fill-rule:evenodd;stroke:#8900ad;stroke-width:1.00000003pt;stroke-opacity:1"
151 d="M 0,-7.0710768 -7.0710894,0 0,7.0710589 7.0710462,0 Z"
152 id="path3060"
153 inkscape:connector-curvature="0" />
154 </marker>
155 <marker
156 inkscape:isstock="true"
157 style="overflow:visible"
158 id="marker3046"
159 refX="0"
160 refY="0"
161 orient="auto"
162 inkscape:stockid="DiamondL">
163 <path
164 transform="scale(0.8)"
165 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
166 d="M 0,-7.0710768 -7.0710894,0 0,7.0710589 7.0710462,0 Z"
167 id="path3044"
168 inkscape:connector-curvature="0" />
169 </marker>
170 <marker
171 inkscape:isstock="true"
172 style="overflow:visible"
173 id="marker2992"
174 refX="0"
175 refY="0"
176 orient="auto"
177 inkscape:stockid="Arrow2Lend">
178 <path
179 transform="matrix(-1.1,0,0,-1.1,-1.1,0)"
180 d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
181 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
182 id="path2990"
183 inkscape:connector-curvature="0" />
184 </marker>
185 <marker
186 inkscape:isstock="true"
187 style="overflow:visible"
188 id="marker2888"
189 refX="0"
190 refY="0"
191 orient="auto"
192 inkscape:stockid="Arrow2Lend">
193 <path
194 transform="matrix(-1.1,0,0,-1.1,-1.1,0)"
195 d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
196 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
197 id="path2886"
198 inkscape:connector-curvature="0" />
199 </marker>
200 <marker
201 inkscape:stockid="DiamondL"
202 orient="auto"
203 refY="0"
204 refX="0"
205 id="marker2374"
206 style="overflow:visible"
207 inkscape:isstock="true">
208 <path
209 id="path2372"
210 d="M 0,-7.0710768 -7.0710894,0 0,7.0710589 7.0710462,0 Z"
211 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
212 transform="scale(0.8)"
213 inkscape:connector-curvature="0" />
214 </marker>
215 <marker
216 inkscape:stockid="DiamondLstart"
217 orient="auto"
218 refY="0"
219 refX="0"
220 id="marker1534"
221 style="overflow:visible"
222 inkscape:isstock="true">
223 <path
224 id="path1532"
225 d="M 0,-7.0710768 -7.0710894,0 0,7.0710589 7.0710462,0 Z"
226 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
227 transform="matrix(0.8,0,0,0.8,5.6,0)"
228 inkscape:connector-curvature="0" />
229 </marker>
230 <marker
231 inkscape:stockid="DiamondLstart"
232 orient="auto"
233 refY="0"
234 refX="0"
235 id="DiamondLstart"
236 style="overflow:visible"
237 inkscape:isstock="true">
238 <path
239 id="path1312"
240 d="M 0,-7.0710768 -7.0710894,0 0,7.0710589 7.0710462,0 Z"
241 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
242 transform="matrix(0.8,0,0,0.8,5.6,0)"
243 inkscape:connector-curvature="0" />
244 </marker>
245 <marker
246 inkscape:stockid="DiamondLend"
247 orient="auto"
248 refY="0"
249 refX="0"
250 id="DiamondLend"
251 style="overflow:visible"
252 inkscape:isstock="true">
253 <path
254 id="path1321"
255 d="M 0,-7.0710768 -7.0710894,0 0,7.0710589 7.0710462,0 Z"
256 style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
257 transform="matrix(0.8,0,0,0.8,-5.6,0)"
258 inkscape:connector-curvature="0" />
259 </marker>
260 </defs>
261 <sodipodi:namedview
262 id="base"
263 pagecolor="#ffffff"
264 bordercolor="#666666"
265 borderopacity="1.0"
266 inkscape:pageopacity="0.0"
267 inkscape:pageshadow="2"
268 inkscape:zoom="0.78940734"
269 inkscape:cx="436.76989"
270 inkscape:cy="259.42716"
271 inkscape:document-units="mm"
272 inkscape:current-layer="layer1"
273 showgrid="false"
274 units="in"
275 inkscape:snap-bbox="true"
276 inkscape:bbox-paths="true"
277 inkscape:bbox-nodes="true"
278 inkscape:snap-bbox-edge-midpoints="true"
279 inkscape:snap-bbox-midpoints="true"
280 inkscape:object-paths="true"
281 inkscape:snap-intersection-paths="true"
282 inkscape:snap-smooth-nodes="true"
283 inkscape:snap-midpoints="true"
284 showguides="false"
285 inkscape:guide-bbox="true"
286 inkscape:snap-nodes="true"
287 inkscape:snap-global="true"
288 inkscape:snap-page="true"
289 inkscape:window-width="1920"
290 inkscape:window-height="1057"
291 inkscape:window-x="-8"
292 inkscape:window-y="-8"
293 inkscape:window-maximized="1">
294 <sodipodi:guide
295 position="3.012537,95.789399"
296 orientation="1,0"
297 id="guide900"
298 inkscape:locked="false" />
299 <sodipodi:guide
300 position="47.826414,90.570182"
301 orientation="1,0"
302 id="guide974"
303 inkscape:locked="false" />
304 </sodipodi:namedview>
305 <metadata
306 id="metadata5">
307 <rdf:RDF>
308 <cc:Work
309 rdf:about="">
310 <dc:format>image/svg+xml</dc:format>
311 <dc:type
312 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
313 <dc:title></dc:title>
314 </cc:Work>
315 </rdf:RDF>
316 </metadata>
317 <g
318 inkscape:label="Layer 1"
319 inkscape:groupmode="layer"
320 id="layer1"
321 transform="translate(0,-119.19998)">
322 <text
323 xml:space="preserve"
324 style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:1.25;font-family:Cambria;-inkscape-font-specification:Cambria;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
325 x="25.385368"
326 y="213.61481"
327 id="text924"><tspan
328 sodipodi:role="line"
329 id="tspan922"
330 x="25.385368"
331 y="223.16978"
332 style="font-size:3.17499995px;text-align:center;text-anchor:middle;stroke-width:0.26458332" /></text>
333 <g
334 id="g8075"
335 transform="matrix(2,0,0,2,0.52231507,-296.99998)">
336 <path
337 inkscape:connector-curvature="0"
338 id="path7520"
339 d="m 25.419474,231.19369 v 10.61269"
340 style="fill:none;stroke:#000000;stroke-width:0.37011486px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
341 <g
342 transform="translate(0,25.472538)"
343 id="g7344">
344 <g
345 id="g7308">
346 <g
347 id="g7302">
348 <rect
349 y="183.30801"
350 x="2.5237961"
351 height="22.340609"
352 width="44.668827"
353 id="rect961"
354 style="opacity:1;fill:#440055;fill-opacity:1;stroke:#000000;stroke-width:0.14504954;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
355 <path
356 inkscape:connector-curvature="0"
357 id="path7240"
358 d="m 25.419474,202.4418 2.592695,2.5927 -2.592695,2.59269 -2.592695,-2.59269 z"
359 style="opacity:1;fill:#440055;fill-opacity:1;stroke:#000000;stroke-width:0.12034056;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
360 </g>
361 <rect
362 y="182.69997"
363 x="3.0850618"
364 height="22.340609"
365 width="44.668827"
366 id="rect888"
367 style="opacity:1;fill:#8900ad;fill-opacity:1;stroke:#000000;stroke-width:0.14504954;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
368 </g>
369 <g
370 id="g7323">
371 <text
372 id="text904"
373 y="188.20667"
374 x="8.0725441"
375 style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:8.25823498px;line-height:1.25;font-family:'Lucida Handwriting';-inkscape-font-specification:'Lucida Handwriting';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.20645587"
376 xml:space="preserve"><tspan
377 style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.27566671px;font-family:Consolas;-inkscape-font-specification:Consolas;stroke-width:0.20645587"
378 y="188.20667"
379 x="8.0725441"
380 id="tspan902"
381 sodipodi:role="line">__yield_files()</tspan></text>
382 <g
383 transform="translate(0,-14.235773)"
384 id="g1016">
385 <rect
386 y="204.49596"
387 x="4.2869978"
388 height="13.458568"
389 width="42.264957"
390 id="rect978"
391 style="opacity:0.51999996;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.10951063;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
392 <g
393 id="g1004">
394 <text
395 xml:space="preserve"
396 style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:1.25;font-family:Cambria;-inkscape-font-specification:Cambria;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
397 x="25.385368"
398 y="207.78415"
399 id="text936"><tspan
400 sodipodi:role="line"
401 id="tspan934"
402 x="25.385368"
403 y="207.78415"
404 style="font-size:3.17499995px;text-align:center;text-anchor:middle;stroke-width:0.26458332">Walks through the content</tspan></text>
405 <text
406 id="text940"
407 y="210.6956"
408 x="25.385368"
409 style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:1.25;font-family:Cambria;-inkscape-font-specification:Cambria;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
410 xml:space="preserve"><tspan
411 style="font-size:3.17499995px;text-align:center;text-anchor:middle;stroke-width:0.26458332"
412 y="210.6956"
413 x="25.385368"
414 id="tspan938"
415 sodipodi:role="line">folder, yielding valid files and</tspan></text>
416 <text
417 xml:space="preserve"
418 style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:1.25;font-family:Cambria;-inkscape-font-specification:Cambria;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
419 x="25.385368"
420 y="213.61481"
421 id="text944"><tspan
422 sodipodi:role="line"
423 id="tspan942"
424 x="25.385368"
425 y="213.61481"
426 style="font-size:3.17499995px;text-align:center;text-anchor:middle;stroke-width:0.26458332">their path relative path sorted</tspan></text>
427 <text
428 id="text948"
429 y="216.49991"
430 x="25.385368"
431 style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:1.25;font-family:Cambria;-inkscape-font-specification:Cambria;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
432 xml:space="preserve"><tspan
433 style="font-size:3.17499995px;text-align:center;text-anchor:middle;stroke-width:0.26458332"
434 y="216.49991"
435 x="25.385368"
436 id="tspan946"
437 sodipodi:role="line">by their last modified date.</tspan></text>
438 </g>
439 </g>
440 </g>
441 </g>
442 </g>
443 <g
444 id="g7493"
445 transform="matrix(2,0,0,2,0.52231507,-298.53759)">
446 <rect
447 y="275.35565"
448 x="2.5237961"
449 height="22.340609"
450 width="44.668827"
451 id="rect7426"
452 style="opacity:1;fill:#440055;fill-opacity:1;stroke:#000000;stroke-width:0.14504954;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
453 <rect
454 style="opacity:1;fill:#8900ad;fill-opacity:1;stroke:#000000;stroke-width:0.14504954;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
455 id="rect7432"
456 width="44.668827"
457 height="22.340609"
458 x="3.0850618"
459 y="274.74759" />
460 </g>
461 <text
462 id="text7438"
463 y="261.97098"
464 x="25.145998"
465 style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:16.51646996px;line-height:1.25;font-family:'Lucida Handwriting';-inkscape-font-specification:'Lucida Handwriting';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.41291174"
466 xml:space="preserve"><tspan
467 style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:8.55133343px;font-family:Consolas;-inkscape-font-specification:Consolas;stroke-width:0.41291174"
468 y="261.97098"
469 x="25.145998"
470 id="tspan7436"
471 sodipodi:role="line">sort_blog()</tspan></text>
472 <rect
473 style="opacity:0.51999996;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.21902126;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
474 id="rect7440"
475 width="84.529915"
476 height="26.917135"
477 x="8.5350428"
478 y="266.07803" />
479 <text
480 xml:space="preserve"
481 style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:21.16666603px;line-height:1.25;font-family:Cambria;-inkscape-font-specification:Cambria;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.52916664"
482 x="50.941074"
483 y="281.73026"
484 id="text7444"><tspan
485 sodipodi:role="line"
486 id="tspan7442"
487 x="50.941078"
488 y="281.73026"
489 style="font-size:6.3499999px;text-align:center;text-anchor:middle;stroke-width:0.52916664">Yields sorted the sorted data.</tspan></text>
490 <path
491 sodipodi:nodetypes="ccccc"
492 inkscape:connector-curvature="0"
493 id="path7514"
494 d="M 46.185902,245.34734 50.8,250.27536 55.414097,245.34734 50.8,254.57552 Z"
495 style="opacity:1;fill:#440055;fill-opacity:1;stroke:#000000;stroke-width:0.30965054;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
496 <g
497 id="g8167"
498 transform="matrix(2,0,0,2,0.52231507,-296.99998)">
499 <path
500 inkscape:connector-curvature="0"
501 id="path7547"
502 d="m 25.419474,265.20249 v 8.33831"
503 style="fill:none;stroke:#000000;stroke-width:0.32806706px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
504 <g
505 id="g8142">
506 <g
507 id="g8096">
508 <g
509 transform="translate(0,28.755724)"
510 id="g7812">
511 <g
512 id="g7350"
513 transform="translate(0,30.350694)">
514 <rect
515 y="183.30801"
516 x="2.5237961"
517 height="22.340609"
518 width="44.668827"
519 id="rect7346"
520 style="opacity:1;fill:#440055;fill-opacity:1;stroke:#000000;stroke-width:0.14504954;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
521 <path
522 inkscape:connector-curvature="0"
523 id="path7348"
524 d="m 25.419474,202.4418 2.592695,2.5927 -2.592695,2.59269 -2.592695,-2.59269 z"
525 style="opacity:1;fill:#440055;fill-opacity:1;stroke:#000000;stroke-width:0.12034056;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
526 </g>
527 <rect
528 y="213.05066"
529 x="3.0850618"
530 height="22.340609"
531 width="44.668827"
532 id="rect7352"
533 style="opacity:1;fill:#8900ad;fill-opacity:1;stroke:#000000;stroke-width:0.14504954;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
534 </g>
535 <g
536 transform="translate(0,28.755724)"
537 id="g7789">
538 <text
539 id="text7358"
540 y="218.35509"
541 x="4.4686885"
542 style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.76128912px;line-height:1.25;font-family:'Lucida Handwriting';-inkscape-font-specification:'Lucida Handwriting';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.16903223"
543 xml:space="preserve"><tspan
544 style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.50062919px;font-family:Consolas;-inkscape-font-specification:Consolas;stroke-width:0.16903223"
545 y="218.35509"
546 x="4.4686885"
547 id="tspan7356"
548 sodipodi:role="line">__process_blog_names()</tspan></text>
549 <g
550 id="g7774">
551 <rect
552 y="220.61087"
553 x="4.2869978"
554 height="13.458568"
555 width="42.264957"
556 id="rect7360"
557 style="opacity:0.51999996;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.10951063;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
558 <g
559 transform="translate(0.03720924,0.18315343)"
560 id="g7762">
561 <text
562 xml:space="preserve"
563 style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:1.25;font-family:Cambria;-inkscape-font-specification:Cambria;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
564 x="25.385368"
565 y="223.89906"
566 id="text7364"><tspan
567 sodipodi:role="line"
568 id="tspan7362"
569 x="25.385368"
570 y="223.89906"
571 style="font-size:3.17499995px;text-align:center;text-anchor:middle;stroke-width:0.26458332">For each blog content file,</tspan></text>
572 <text
573 id="text7744"
574 y="226.81827"
575 x="25.385368"
576 style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:1.25;font-family:Cambria;-inkscape-font-specification:Cambria;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
577 xml:space="preserve"><tspan
578 style="font-size:3.17499995px;text-align:center;text-anchor:middle;stroke-width:0.26458332"
579 y="226.81827"
580 x="25.385368"
581 id="tspan7742"
582 sodipodi:role="line">apply a filter to conform the</tspan></text>
583 <text
584 xml:space="preserve"
585 style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:1.25;font-family:Cambria;-inkscape-font-specification:Cambria;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
586 x="25.385368"
587 y="229.72351"
588 id="text7748"><tspan
589 sodipodi:role="line"
590 id="tspan7746"
591 x="25.385368"
592 y="229.72351"
593 style="font-size:3.17499995px;text-align:center;text-anchor:middle;stroke-width:0.26458332">filename to a valid URI</tspan></text>
594 <text
595 id="text7752"
596 y="231.97453"
597 x="25.385368"
598 style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:1.25;font-family:Cambria;-inkscape-font-specification:Cambria;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
599 xml:space="preserve"><tspan
600 style="font-size:3.17499995px;text-align:center;text-anchor:middle;stroke-width:0.26458332"
601 y="231.97453"
602 x="25.385368"
603 id="tspan7750"
604 sodipodi:role="line">endpoint.</tspan></text>
605 </g>
606 </g>
607 </g>
608 </g>
609 <path
610 sodipodi:nodetypes="ccccc"
611 inkscape:connector-curvature="0"
612 id="path7518"
613 d="m 23.112425,239.34237 2.307049,2.46401 2.307049,-2.46401 -2.307049,4.61409 z"
614 style="opacity:1;fill:#440055;fill-opacity:1;stroke:#000000;stroke-width:0.15482527;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
615 </g>
616 </g>
617 </g>
618 </svg>