GitHub

@@ -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

Read the original on github.com ↗