RSS has been around for a little over 25 years at this point, and is my favorite way to grab news from all over the internet. Atom, meanwhile is a little newer at 18 years old. Both have been popular means, and well supported means, to get aggregated content directly to users from the source. Most aggregators (at least all popular and most niche ones) support both directly without issue, and for the rest of this blog I'll be refering to them both as the same tech. Please note however that this will be a guide centered mainly about getting Atom working in Blazor, although with minimal changes RSS support can be offered instead or in addition to Atom.
So let's get started, Atom is built on top of XML which is a document format meant to be easily parsed by computers and people. If you're at all familiar with XML you'll find plenty of similarities. With this specific implementation we're going to go straight into understanding the Atom format enough to build out a working feed, and towards the end I'll provide links to a few references that helped me troubleshoot and develop my feed.
As a reference for the next section here's a sample Atom compatible XML file
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>{Your Website Title}</title>
<link href="https://example.org/atom.xml" rel="self" />
<link href="https://example.org/" />
<id>https://example.org/</id>
<updated>{RFC3339 Compatible Date}</updated>
<entry>
<title>{Entry Title}</title>
<id>{Permanent Link to the entry}</id>
<updated>{RFC3339 Date}</updated>
<summary>{Summary}</summary>
<content type="html">
{HTML Encoded Content}
</content>
<author>
<name>{Entry Author Name}</name>
</author>
</entry>
</feed>
With an atom feed you need to have an XML document header, this will define the contents as XML with a version. After that, we'll have a feed tag that everything sit inside of. We'll start with defining the feed itself using an id, a title, and links to both the site and to the feed itself. Other important tags are going to be the Updated tag, which expects an RFC3339 formatted date (don't worry I'll give you the string formatter for that one). Toss in an Author with a name subtag and you've got a feed. It won't have anything in it yet, so we'll fix that with entries, each one requires a Title, an Id, and an Updated tag.
Let's go ahead and take a look at the Blazor side and how to actually implement a valid Atom feed.
On the Server Side of your Blazor project, if one is not already present add a folder named Controllers. This is mainly for organizational purposes, but not without importance. From there, right click on the folder and click Add> New Item...
From there select API Controller - Empty
I've named mine AtomController.cs and recommend that you do the same. It's important that Controller is present at the end of the name, Blazor requires it to be recognized properly. The first thing we'll do from there is changing the [Routes("api/[controller]")] to be set as [Routes("atom.xml")]. This is the endpoint that the RSS reader will be trying to reach.
namespace YourServerProject.Controllers
{
//[Route("api/[controller]")]
[Route("atom.xml")]
[ApiController]
public class AtomController : ControllerBase{}
}
This is going to be where we add all of the information we want to populate the feed with. So let's start: I'm going to take advantage of dependency injection and add my BlogPostService to the Controller. I'd recommend injecting whatever services you need to get the content you'll serve to users.
From there we're going to add an HttpGet attribute inside of the controller, and follow it with our function that will generate and reply to any request to the endpoint we set.
We're going to create a public async Task which will return an ActionResult holding an XmlDocument. Name the function however you please, you will not be referring to this by function name later.
namespace YourServerProject.Controllers
{
[Route("atom.xml")]
[ApiController]
public class RssController(IBlogPostService blogPostService) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<XmlDocument?>> CreateRssFeed()
{
}
}
}
Let's wrap everything we're about to do with a try so we can return some information if something goes wrong. Add in a console output with the exception, and then we're going to create a custom ContentResult to make sure we're following the expected output of the function we created.
try
{
}
catch (Exception ex)
{
Console.WriteLine(ex);
return new ContentResult { ContentType = "application/xml", Content = ex.Message };
}
And now for the most important part, let's fill out the Xml data that we'll send to the user.
Next is the full chunk of code that I use to generate the Atom feed for this site. I'll explain it underneath, but come back to this as I talk through it.
try
{
PagedList<BlogPostDTO> blogPosts = await blogPostService.GetPublishedPostsAsync(1, 300);
string xml =
"<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<feed xmlns=\"http://www.w3.org/2005/Atom\">" +
"<id>https://blog.smorote.com/</id>" +
"<title>SMorote Blog</title>" +
"<link href=\"https://blog.smorote.com/atom.xml\" rel=\"self\" />" +
"<link href=\"https://blog.smorote.com/\" rel=\"alternate\" />" +
$"<updated>{blogPosts.Data.OrderByDescending(p => p.Created).First().Created.ToString("yyyy-MM-ddTHH:mm:sszzz")}</updated>" +
$"<author><name>Morote</name></author>";
foreach (BlogPostDTO blogPost in blogPosts.Data)
{
string createdDate = blogPost.Created.ToString("yyyy-MM-ddTHH:mm:sszzz");
string content = WebUtility.HtmlEncode(blogPost.Content)!;
xml +=
"<entry>" +
$"<title>{blogPost.Title}</title>" +
$"<id>https://blog.smorote.com/{blogPost.Slug}</id>" +
$"<link href=\"https://blog.smorote.com/{blogPost.Slug}\"/>" +
$"<updated>{createdDate}</updated>" +
$"<summary>{blogPost.Abstract}</summary>" +
$"<content type=\"html\">{content}</content>" +
$"<author><name>{blogPost.AuthorName}</name></author>" +
"</entry>";
}
xml += "</feed>";
return new ContentResult
{
ContentType = "application/atom+xml",
Content = xml,
StatusCode = 200
};
}
catch (Exception ex)
{
Console.WriteLine(ex);
return new ContentResult { ContentType = "application/xml", Content = ex.Message };
}
So the first line, inside of the try, is me bringing in data that I'm going to use to populate the entries. This class is holding a field called Data which holds my list of blogposts. These have a Created date, a slug which is part of their link, an abstract, content, and an author's name.
Then we're going to create a string accross multiple lines, we could add new line characters to format the xml document. I chose not to, as doing it this way leaves it readable in code, and minified in practice to reduce file size.
The first thing you'll notice is a ton of backslashes all over the place, these are escape characters and let us have quotation marks inside of strings.
Remember that document heading? We're starting out with it, you won't need to change it. It defines the rest of this content as Xml so even if we used a different endpoint it would still be read correctly.
The next line after that is the beginning of the feed. If you're going to use Rss this is where you'll really start to change what you're doing.
<feed xmlns="http://www.w3.org/2005/Atom">
This above string gives information about how this feed is supposed to be read and what standard it's expecting.
After that you'll have the <id> tag, this is the first part where we need to talk about links. Best practices dictate that you should be using an absolute link, that is also canonical. This means a few things. Absolute links are links that have the full starting portion versus relative links which give their location relative to where you're already expected to be.
An absolute link to my website from this blog page would be as follows:
A relative link to the same place would instead be:
/
Now onto canonical links, canonical links are meant to ensure that a duplicate version of something is not available at the link, this has to do with query parameters or other information which can be added to a URL. This is pretty easy to do.
A non-canonical link to my portfolio would be:
Whereas a canonical link would be:
A simple change but make sure that when adding links to the document from here on ensure that they are both absolute, and canonical. So with these best practices in mind my <id> tag should look like this:
<id>https://blog.smorote.com/</id>
This id should be a permalink to the site that hosts the Atom feed, not any individual article.
Let's add a title, this will be read by the feed reader and be the title the user sees by default on their feed. I'd recommend the website name, or if you have more than one feed, make it descriptive enough to know that it comes from your website, with something about what the feed actually covers.
We're going to have two links next, one which we'll say is where the feed is hosted, and one which is again saying the parent site.
<link href="https://blog.smorote.com/atom.xml" rel="self" />
<link href="https://blog.smorote.com/" rel="alternate" />"
Next is our first RFC3339 formatted date. The formatted string is yyyy-MM-ddTHH:mm:sszzz
So in my example I'm looking at my array of blogposts, organizing them in descending order by date, grabbing the most recent one, then finally formatting the DateTimeOffset with a .ToString("yyyy-MM-ddTHH:mm:sszzz"). So grab whatever DateTimeOffset you'd like and convert it to an RFC3339 date and drop it into an <updated> tag. I'd recommend making sure this always references your most recent post for it's date.
After that we have to drop a site/feed author tag in with a name.
<author>
<name>Morote</name>
</author>
From here you have a feed! There's still more setting up to do, but this is the very minimum to hit the Atom specification.
Now that you have the idea, let's run through entries in Atom.
<entry>
<title>{blogPost.Title}</title>
<id>https://blog.smorote.com/{blogPost.Slug}</id>
<link href="https://blog.smorote.com/{blogPost.Slug}"/>
<updated>{createdDate}</updated>
<summary>{blogPost.Abstract}</summary>
<content type="html">{content}</content>
<author>
<name>{blogPost.AuthorName}</name>
</author>
</entry>
The title tag is fairly self explanatory, and should contain the title for the post itself. A mistake I've seen often with some feeds is that they will include the feed or site name in entry titles, this is superfluous information and does not need to be added to each entry. The <id> tag is expected to be a canonical permalink to the entry content. The <link> tag meant to link to a related document by default, but a rel or relationship attribute can specify what the link is referencing. Most feeds just use it as an additional link to the article or entry, and that's what I'm doing here.
The updated tag requires an RFC3339 compliant date, so let's make one with C#. If you have a DateTimeOffset it's as easy as using a ToString with the aforementioned format string. On the offchance you don't remember it offhand after seeing it just once, here's my line for assigning it to a variable I used.
string createdDate = blogPost.Created.ToString("yyyy-MM-ddTHH:mm:sszzz");
And now one of my favorite bits, HTML encoding a string. System.Net.WebUtility includes a wonderful method for this, HtmlEncode. So this can be done inline, but for ease of reading I assigned it to a variable with this line from above.
string content = WebUtility.HtmlEncode(blogPost.Content)!;
After we've done these things and put them in the right places, we just need to add an author tag, with a name subtag. I would like to highlight that if you have multiple author's on an entry you can, and should have them in separate tags for each author. Then we can close up the entry.
Add as many entries as you need, but keep in mind that many feed readers may not want to handle a file that is too large. Once you've done this add the closing feed tag to your XML string.
Now that the XML string is formatted and ready, let's create a custom ContentResult to make sure that we are sending a good network response. Although many feed readers and browsers can handle malformed or entirely wrong headers with XML content, it's best to just get things right to begin with.
We're going to return a new ContentResult, setting the ContentType to "application/atom+xml" this tells the receiver what kind of information should be received and by default how it should be parsed. We'll set the Content to our XML we created earlier, and the StatusCode to 200 indicating a success.
return new ContentResult
{
ContentType = "application/atom+xml",
Content = xml,
StatusCode = 200
};
And with that our controller is done! Let's configure a few things to make sure everything works as expected though. Let's add a line of HTML to our home page or our App.razor that let's the browser know that there is an Atom feed associated with this site. This tag won't appear on the page, but browser extensions or RSS readers will look for this to know where the feed is located.
<link href="atom.xml" type="application/atom+xml" rel="alternate" title="Atom Feed" />
We're going to use a relative link this time, although if you want to have this feed available on another site you can use an absolute link instead. Title the feed however you'd like, if you have multiple feeds I'd recommend distiguishing between them in some way. This won't have any effect on the feed after it is in a feed reader, but if someone is using an extension to grab one, this will be displayed as the option.
If you've already set up controllers in your project, great you're done and can skip to the end to validate your feed and do some last minute troubleshooting, but if not then there is just one thing left to do.
Let's go to the Program.cs on your server and add the line
app.MapControllers();
after the line building your app. If you're using a different variable to refer to your web application, just ensure that controllers are being mapped properly.
Congratulations, you now should have a working Atom feed for your website. If not, or if you'd like to expand it out further here's some resources that helped me a ton when figuring this out.
First and foremost the W3C Feed Validator was hugely helpful in figuring out where things were going wrong. Not only will it tell you what errors are occuring but it also has helpful pages with information on how to fix it. Here is their page with an introduction to Atom, which is incredibly helpful for refering to how it is meant to be used.
Probably my favorite find in this process however was a fantastic blog post by Kevin Cox I stumbled across. His post talks about best practices and even includes aspects of the standard that are often missed.
If you enjoyed reading this or it helped you feel free to subscribe to my Atom feed!
1 Comment
Join the conversation, login to comment!Jacob Visick
10/14/2024 21:05Great idea! It's awesome to see how simple it can be implementing a standard that brands itself as "Really Simple." You may want to consider using a StringBuilder when concatenating many strings together, or even using an XmlWriter for a more robust solution. And for the cherry on top, an RSS feed may be a prime candidate for output caching so you don't have to recreate the XML on every request :) https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-xml-xmlwriter https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder?view=net-8.0 https://learn.microsoft.com/en-us/aspnet/core/performance/caching/output?view=aspnetcore-8.0