@@ -0,0 +1,298 @@
1+title: Creating an RSS (Atom) Feed
2+slug: creating-an-rss-feed
3+created: 2019-07-30
4+updated: 2019-07-30
5+tags: rust, rss, programming, blog, atom
6+summary: creating an RSS (Atom) feed from scratch
7+8+# Creating an RSS (Atom) Feed
9+10+The main way that I keep up with blogs is through RSS. I have been using
11+the excellent [Reeder](https://reederapp.com/) since whenever Google Reader
12+shut down, with [Feedly](https://feedly.com/i/welcome) as the backend.
13+Naturally, I therefore would like for my own blog to have an
14+RSS feed. This serves two purposes: one, it helps any interested people to
15+keep up with the blog, and two, if I put it in my feed, it will give me
16+a second opportunity to proofread things that I post.
17+18+This post will follow the process of making an RSS feed from scratch.
19+I figured that, in the spirit of this generally handcrafted blog experience,
20+I might as well make it myself rather than going with some third-party
21+service.
22+23+## RSS or Atom
24+25+One very quickly learns that there are two competing specifications in the
26+real simple syndication world. [RSS 2.0][RSS Specification], published in
27+2003, was the successor to the original RSS specification and was the
28+dominant player prior to [Atom][Atom Specification], published in 2005 with
29+what seems to my uneducated eye to be a bit more of a formal specification.
30+31+Web searches like "rss vs atom" aren't particularly enlightening as to what
32+might lead one to pick one or the other feed format, although it does
33+seem to be a topic that several blog-related companies have written fluff
34+pieces about in order to improve their SEO, which I will refrain from linking
35+here. Fluff aside, I did manage to find a [more substantive discussion](https://github.com/jekyll/jekyll-feed/issues/2)
36+on the [jekyll-feed GitHub repository](https://github.com/jekyll/jekyll-feed).
37+There's also [this post from the _null program_ blog](https://nullprogram.com/blog/2013/09/23/),
38+which looks at some of the details about the pain points in the [RSS specification]
39+as compared to the [Atom specification]. There's also a [question from 2010](https://wordpress.stackexchange.com/questions/2922/should-i-provide-rss-or-atom-feeds)
40+on the Wordpress StackExchange.
41+42+From reading through those links, what I've gathered is mostly that:
43+44+* Basically any feed aggregator/reader supports both Atom and RSS feeds
45+* Publishing podcasts on iTunes requires an RSS feed
46+* The Atom specification is more well defined and by most accounts easier
47+ to work with
48+49+Given all of that, I think that I'll probably start with Atom, and then
50+potentially add an RSS feed as well later on.
51+52+## Implementing Atom
53+54+I started out by reading the [Atom specification], which is actually quite
55+an easy read. It gives some examples of Atom documents, one of which I
56+copied over to form the base of my template. From there, I started reading
57+up on the definitions for the various elements of the specification. There
58+are top-level metadata attributes that can be specified for a feed, most
59+of which are fairly straightforward (`author`, `title`, `updated`, etc.).
60+Feed entries are specified in an `entry` element, which contains information
61+about the individual entry.
62+63+### Unique IDs
64+65+One of the components of the Atom specification is that each entry have
66+a unique ID. This honestly isn't something I planned for when I was
67+originally coding the site, because none of the content exists in a
68+database. However, there is an assumption that each blog post's slug
69+will be unique, since the slugs are used as the path in the URL.
70+71+The [atom specification] has this to say about the ID element:
72+73+> Its content MUST be an IRI, as defined by [RFC3987]. Note that the
74+> definition of "IRI" excludes relative references. Though the IRI
75+> might use a dereferencable scheme, Atom Processors MUST NOT assume it
76+> can be dereferenced.
77+>
78+> When an Atom Document is relocated, migrated, syndicated,
79+> republished, exported, or imported, the content of its atom:id
80+> element MUST NOT change. Put another way, an atom:id element
81+> pertains to all instantiations of a particular Atom entry or feed;
82+> revisions retain the same content in their atom:id elements. It is
83+> suggested that the atom:id element be stored along with the
84+> associated resource.
85+>
86+> The content of an atom:id element MUST be created in a way that
87+> assures uniqueness.
88+89+That description, along with their example ID of
90+`urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a`, got me going down a bit
91+of a rabbit hole of trying to figure out how to hash the slug and
92+represent it as an IRI, but the [w3c validator introduction to atom][w3c atom intro]
93+makes things a lot less scary:
94+95+> Identifies the entry using a universally unique and permanent URI.
96+> Suggestions on how to make a good id can be found here. Two entries in
97+> a feed can have the same value for id if they represent the same entry
98+> at different points in time.
99+100+Their example ID is just `<id>http://example.com/blog/1234</id>`. Since
101+we have unique URIs for our posts, we can just use those. Phew!
102+103+### Templates
104+105+Atom's a pretty simple format, so we just need two templates, one for the
106+page and another for entries:
107+108+```html
109+<!-- atom.xml -->
110+111+<?xml version="1.0" encoding="utf-8"?>
112+<feed xmlns="http://www.w3.org/2005/Atom">
113+114+<title>Matthew Planchard's Blog</title>
115+<link rel="self" href="https://blog.mplanchard.com/atom.xml"/>
116+<link href="https://blog.mplanchard.com/"/>
117+<updated>{{ updated }}</updated>
118+<author>
119+<name>Matthew Planchard</name>
120+</author>
121+<id>https://blog.mplanchard.com/</id>
122+123+ {{ entries }}
124+125+</feed>
126+```
127+128+```html
129+<!-- atom-entry.xml -->
130+131+<entry>
132+<title>{{ title }}</title>
133+<link href="{{ link }}"/>
134+<id>{{ link }}</id>
135+<updated>{{ updated }}</updated>
136+<summary>{{ summary }}</summary>
137+</entry>
138+```
139+140+### The Rust!
141+142+With all of the refactoring we did as part of
143+[adding tag support](/posts/adding-support-for-tags-1.html), this is
144+pretty easy.
145+146+First, we add our page template to our
147+[`TemplatePageStrings` struct](https://github.com/mplanchard/speedy/blob/ed60e0eb767a60e1371401b0741c2add86d01b08/src/main.rs#L114-L119)
148+and its [const instance](https://github.com/mplanchard/speedy/blob/ed60e0eb767a60e1371401b0741c2add86d01b08/src/main.rs#L146-L151),
149+and add a parser to our `PageTemplates` struct:
150+151+```rust
152+struct TemplatePageStrings {
153+about: &'static str,
154+atom: &'static str,
155+generic: &'static str,
156+index: &'static str,
157+post: &'static str,
158+}
159+// ...
160+const TEMPLATE_STRINGS: TemplateStrings = TemplateStrings {
161+// ...
162+pages: TemplatePageStrings {
163+about: include_str!("../templates/pages/about.html"),
164+atom: include_str!("../templates/pages/atom.xml"),
165+generic: include_str!("../templates/pages/generic.html"),
166+index: include_str!("../templates/pages/index.html"),
167+post: include_str!("../templates/pages/post.html"),
168+ },
169+170+// ...
171+struct PageTemplates {
172+about: liquid::Template,
173+atom: liquid::Template,
174+generic: liquid::Template,
175+index: liquid::Template,
176+post: liquid::Template,
177+}
178+impl PageTemplates {
179+fn new(parser: &liquid::Parser) -> Self {
180+let parse = |template_str| parse_template_str(parser, template_str);
181+Self {
182+about: parse(TEMPLATE_STRINGS.pages.about),
183+atom: parse(TEMPLATE_STRINGS.pages.atom),
184+generic: parse(TEMPLATE_STRINGS.pages.generic),
185+index: parse(TEMPLATE_STRINGS.pages.index),
186+post: parse(TEMPLATE_STRINGS.pages.post),
187+ }
188+ }
189+}
190+```
191+192+As a side note, we might eventually do a post on macros to make it easier
193+to get these templates made :)
194+195+From there, we do the same thing for the snippet template in its struct
196+and instance, although I won't bore you with that.
197+198+We need to add two rendering methods to the `Context` struct, one for
199+atom entries and another for the atom page. The first looks like this:
200+201+```rust
202+impl<'a> Context<'a> {
203+// ...
204+fn render_atom_entry(&self, post: &Post) -> String {
205+let globals = liquid::value::Object::from_iter(vec![
206+ ("title".into(), to_liquid_val(&post.metadata.title)),
207+ ("link".into(), to_liquid_val(&post.url)),
208+ (
209+"updated".into(), to_liquid_val(
210+DateTime::<Utc>::from_utc(
211+post.metadata.updated.and_hms(0, 0, 0), Utc
212+ ).to_rfc3339())
213+ ),
214+ ("summary".into(), to_liquid_val(&post.metadata.summary)),
215+ ]);
216+self.templates
217+.snippets
218+.atom_entry
219+.render(&globals)
220+.expect(&format!("failed to reader atom entry for {:?}", post))
221+ }
222+// ...
223+```
224+225+Because I only store the date of the last update, and the atom spec wants
226+a datetime, I decided just to represent it as the time at midnight UTC,
227+for entirely arbitrary reasons.
228+229+Rendering a _page_ is a little more complicated, at least if we want to
230+avoid adding multiple iterations through our posts. This is because one
231+of the things that the spec reuqires is an `<updated>` tag for the feed
232+as a whole. This is fine, because I store this information, but the posts
233+are stored in the `Context` struct in order by creation date. So, I need
234+to iterate over them, get the most recent update date, and also render
235+them all into `atom-entry` format, ideally all in one loop, and ideally
236+functionally!
237+238+I wound up doing this by bifurcating the result of a `.fold()`:
239+240+```rust
241+fn render_atom_page(&self) -> String {
242+// Get the most recently updated entry and a string of rendered
243+// <entry> documents, separated by newlines.
244+let (updated, entries) = self.posts.iter().fold(
245+ (NaiveDate::from_ymd(1, 1, 1), String::new()),
246+|(newest_date, entries), post| {
247+ (
248+if post.metadata.updated > newest_date {
249+post.metadata.updated
250+ } else {
251+newest_date
252+ },
253+ [entries, self.render_atom_entry(post)].join("\n"),
254+ )
255+ },
256+ );
257+let globals = liquid::value::Object::from_iter(vec![
258+ (
259+"updated".into(),
260+to_liquid_val(Self::updated_datetime_str(&updated)),
261+ ),
262+ ("entries".into(), to_liquid_val(entries)),
263+ ]);
264+self.templates
265+.pages
266+.atom
267+.render(&globals)
268+.expect("failed to render atom feed")
269+ }
270+```
271+272+Amazingly, once the compiler was satisfied, this worked on the first try!
273+I honestly don't think I could have written something like this in Python,
274+which has been my primary language at work for five years, and gotten it
275+to work without extensive testing and tweaking.
276+277+From there, it's just a matter of adding links to the RSS feed from the
278+main page!
279+280+## Conclusions
281+282+The [atom specification] is thorough and easy to follow, although you'll
283+probably still want some extra resources (like the [W3C atom intro]) for
284+when you get confused.
285+286+The architecture of my site generator is coming along nicely. There's still
287+a fair bit of what feels like busy work when adding a new template, because
288+it needs to be added in one way or another in three separate places.
289+Making this smoother with a macro or something similar might be the focus
290+of a future piece.
291+292+Rust is a great functional language! Sometimes you've got to really think
293+about how to do something functional, but it's almost always possible.
294+295+[Atom Specification]: https://tools.ietf.org/html/rfc4287
296+[IRI Specification]: https://tools.ietf.org/html/rfc3987
297+[RSS Specification]: https://cyber.harvard.edu/rss/rss.html
298+[W3C Atom Intro]: https://validator.w3.org/feed/docs/atom.html