(RSS) Feed the Beast

2026-02-04

git commit -m "Rewrite project in Rust."

source code

Oops, I seem to have slipped, fallen, and accidentally used the Rocket web framework in combination with the maddi-templating crate to rewrite my personal blog (this blog) in Rust.

mustache and markdown-rs do some of the heavy lifting, and we've leveraged maddi-templating to add an RSS feed along the way.

Why Tho?

The rewrite was primarily motivated by a want to add an RSS feed, but also for a chance to use the macro I wrote in the last blog post.

Go seems a fine enough language, but I'm just more fluent in Rust at the moment. Given the source was only around 250 lines of Go, it seems worth a quick rewrite now that I'm spending so much time writing posts.

So What's RSS?

RSS is a specification for defining news 'channels' in XML files. This allows apps that can read this format to download lists of articles from multiple different sources and notify a user when anything new is published.

While it was common in the earlier days of the web, it has largely gone out of fashion, now mostly only used in the tech sphere.

Because RSS is just XML, we can use mustache and the maddi-templating macro crate from the last blog post to build the RSS endpoint.

Upsides and Downsides

The new Rust implementation of the blogs uses the Rocket web framework. This allows us to implement a fault-tolerant, multi-threaded server in very few lines of code.

With maddi-templating to abstract away some of the boilerplate, the first version without an RSS feed actually ended up being fewer lines of code than the Go version.

The downside however is that while the compilation for the Go port is almost instant, compiling the Rocket version can (ironically) take minutes on the old x86 machine I use as my server. Usually I'd compile on my development machine but as I write on an aarch64 machine and deploy to NixOS, that becomes rather difficult.

Porting the Index

I started with the templates. For index.html, I switched from using htmx and just inlined the code to display the post previews.


<!--
SPDX-FileCopyrightText: 2026 Madeline Baggins <madeline@baggins.family>

SPDX-License-Identifier: AGPL-3.0-only
-->

<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="/style.css">
  <!--
    ...js syntax highlighting scripts...
  -->
</head>
<body>
  <div class="content">
  {{&index}}
  <br>
  {{#previews}}
    <div class="preview" onclick="window.location.href='/post/{{slug}}'">
      {{&body}}
    </div>
  {{/previews}}
  </div>
  <script>hljs.highlightAll();</script>
</body>
</html>

Note here the usage of & in {{&body}}. This tells the mustache templating engine not to sanitize the text that's included because we'll be inserting raw HTML for the blog previews.

Porting the post.html was similar as we're simply inserting raw HTML into the body of the page.

We'll use maddi-templating to define the templated structs with:

#[derive(Serialize)]
struct Preview {
    slug: String,
    body: String,
}

#[template("./index.html")]
struct Index {
    index: String,
    previews: Vec<Preview>,
}

We can then define a route for / with Rocket via

#[get("/")]
fn index(args: &State<Args>) -> Option<Rendered<Index>> {
    Some(Index::build(args).ok()?.into())
}

If you'd like to know the details of populating the Index struct, check out the source here.

Other Routes

The two other routes needed to reimplement the blog is /post/<slug> and /style.css.

For /style.css, we define a route that returns a RawCss wrapping a Cow str to allow users to define their own CSS if needed.

#[get("/style.css")]
fn style(args: &State<Args>) -> RawCss<Cow<'static, str>> {
    let path = args.content.join("style.css");
    match std::fs::read_to_string(path) {
        Ok(user_css) => RawCss(user_css.into()),
        _ => RawCss(include_str!("./style.css").into()),
    }
}

Finally, we reimplement the /post/<slug> route with #[get("/post/<slug>")] from Rocket and use the markdown crate to return the populated Post struct.

#[template("./post.html")]
struct Post(String);

A nice thing about the use of a Templated trait in maddi-templating is that we can actually define the templated struct inside the body of the function that returns it and just use impl Templated to describe the return type.

#[get("/post/<slug>")]
fn post(args: &State<Args>, slug: String) -> Option<Rendered<impl Templated>> {
  //...
}

Adding The RSS Feed

Because the RSS feed is XML, we can use maddi-templating and mustache exactly how we would with HTML.

// main.rs
// ...
#[template("./rss.xml")]
struct Rss {
    title: String,
    description: String,
    url: String,
    date: String,
    ttl: u32,
    items: Vec<RssItem>,
}

#[derive(Serialize)]
struct RssItem {
    title: String,
    link: String,
    date: String,
}
// ...
<!-- rss.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
 <title>{{title}}</title>
 <description>{{description}}</description>
 <link>{{url}}</link>
 <copyright>2026 Madeline Baggins (maddie@baggins.family) </copyright>
 <lastBuildDate>{{date}}</lastBuildDate>
 <pubDate>{{date}}</pubDate>
 <ttl>{{ttl}}</ttl>

 {{#items}}
 <item>
  <title>{{title}}</title>
  <link>{{link}}</link>
  <pubDate>{{date}}</pubDate>
 </item>
 {{/items}}

</channel>
</rss>

If you'd like to know about populating the RSS struct, see main.rs.



If at first you don't succeed...

Just rewrite it in Rust!