Home
Head's Up: I'm in the middle of upgrading my site. Most things are in place, but there are something missing and/or broken including image alt text. Please bear with me while I'm getting things fixed.

Create An Atom Feed In Rust

Introduction

This is what I'm doing to make an Atom feed for my site. More notes below the code.

rust
//! ```cargo
//! [dependencies]
//! atom_syndication = "0.12.2"
//! chrono = { version = "0.4.26", features = ["std"] }
//! uuid = { version = "1.6.1", features = ["v5"] }
//! ```

use atom_syndication::*;
use chrono::prelude::*;
use std::collections::BTreeMap;
use std::fs;
use uuid::Uuid;


fn main() {

  let feed_constant_uuid4 = Uuid::try_parse(
    "b71fc261-fc1a-4220-b43f-2be4b216c0b2"
  ).unwrap();

  let generation_date = chrono::offset::Utc::now().fixed_offset();

  let author = Person {
    name: "Alan W. Smith".to_string(), 
    email: None,
    uri: Some("https://www.example.com/".to_string())
  };

  let entry_uuid5 = format!("urn:uuid:{}", Uuid::new_v5(
    &feed_constant_uuid4, b"unque_file_id_alfa")
      .as_hyphenated()
      .to_string()
  );

  let entry_title = Text {
    value: "This Is A Title".to_string(),
    base: None, 
    lang: None,
    r#type: TextType::Text
  };

  let tz_hour_offset = 5; 
  let tz = FixedOffset::west_opt(tz_hour_offset * 60 * 60).unwrap();
  let updated_date = NaiveDateTime::parse_from_str("2023-12-17 10:33:55", "%Y-%m-%d %H:%M:%S")
    .unwrap()
    .and_local_timezone(tz)
    .unwrap();

  let entry_content = Some(
    Content {
      base: None, 
      lang: None, 
      value: Some("<p>Hello, world</p>".to_string()),
      src: Some("https://www.example.com/some_path.html".to_string()),
      content_type: Some("html".to_string()),
    }
  );

  let entry = Entry {
      id: entry_uuid5,
      title: entry_title,
      authors: vec![author.clone()],
      categories: vec![],
      content: entry_content,
      summary: None, 
      source: None,
      rights: None, 
      published: None, 
      links: vec![],
      contributors: vec![],
      updated: updated_date,
      extensions: BTreeMap::new() 
  };

  let entries = vec![entry];

  let feed_id = format!(
    "urn:uuid:{}",
    feed_constant_uuid4.as_hyphenated()
  );

  let feed_link = Link{
    href: "https://www.example.com/atom.xml".to_string(),
    rel: "self".to_string(),
    hreflang: None, 
    length: None, 
    mime_type: None,
    title: None
  };

  let feed = Feed {
    title: "My Example Feed".into(),
    id: feed_id,
    updated: generation_date,
    authors: vec![author],
    links: vec![feed_link],
    entries,
    ..Default::default()
  };

  let config = WriteConfig {
    write_document_declaration: true,
    indent_size: Some(2),
  };

  let file = fs::OpenOptions::new()
    .create(true)
    .truncate(true)
    .write(true)
    .open("/Users/alan/Desktop/atom-test.xml").unwrap();

  // output the file
  feed.write_with_config(&file, config).unwrap();

  // print it for demo
  println!("{}", feed.to_string());
}

Results

xml
<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>My Example Feed</title>
  <id>urn:uuid:b71fc261-fc1a-4220-b43f-2be4b216c0b2</id>
  <updated>2024-01-02T23:29:27.999342+00:00</updated>
  <author>
    <name>Alan W. Smith</name>
    <uri>https://www.example.com/</uri>
  </author>
  <link href="https://www.example.com/atom.xml" rel="self"/>
  <entry>
    <title>This Is A Title</title>
    <id>urn:uuid:b1507332-d46c-5bcf-929a-f89c59e01bac</id>
    <updated>2023-12-17T10:33:55+05:00</updated>
    <author>
      <name>Alan W. Smith</name>
      <uri>https://www.example.com/</uri>
    </author>
    <content type="html" src="https://www.example.com/some_path.html">&lt;p&gt;Hello, world&lt;/p&gt;</content>
  </entry>
</feed>

- This is for an Atom feed. A similar crate exists for RSS (linked below)

- I went with Atom based off > this post > https : //peterbabic.dev/blog/using - uuid - in - atom - feed/ >

- The UUIDs work by creating a UUID4 that stays the same every time the feed is generated and then a UUID5 for each file based off an ID that's attached to the file

- The UUID5 is a combination of the main feeds UUID4 + the unique ID for the file. Given those two inputs the output is always the same. That lets the individual entry IDs stay the same even if their content changes.

- The [TODO: Code shorthand span ] is a [TODO: Code shorthand span ] which is an alias of [TODO: Code shorthand span ] from the chrono crate

- My content file dates don't have a timezone (e.g. [TODO: Code shorthand span ] ). The [TODO: Code shorthand span ] is my offset from GMT which I use to calculate the data to get to the [TODO: Code shorthand span ] that's needed

- The rest of the date code converts to the necessary object format that then outputs with a timezone like : [TODO: Code shorthand span ] in the feed

- There's a bunch of things set to [TODO: Code shorthand span ] across the code. The docs (linked below) list all those details

- The content comes out as HTML escaped characters. (e.g. [TODO: Code shorthand span ] becomes [TODO: Code shorthand span ] I've seen a bunch of other feeds that use CDATE, but the escaping works just fine in my reader (NetNewsWire)

- I put two outputs in the example. The first one outputs to a file with a config attached that pretty prints the contents. The second one outputs a string. I haven't figure out a way to pretty print that though

- I'm not sure what the 'rel' should be. I tried "canonical" at first, but it didn't work with RSS Parrot. I've now changed it to "self" which is what I've now seen on other feeds. We'll see what happens

Footnotes And References