FunnelWeb Feeds

When FunnelWeb first became open source, one of the first things we wanted to look at was the way feeds are generated from the site. As PaulPad, the engine had been generating feeds using a custom view. We knew that there was a better way - serving up the XML using a custom ActionResult directly from the controller.

The FeedsController makes use of an underlying repository of type IFeedsRepository. The first thing that happens when you request a feed is a call off to the repository's GetFeed method:

var entries = _feedRepository.GetFeed(feedName, 0, 20);

Now we have a variable (entries) containing the 20 most recent entries on the site. Having retrieved those, we now use a LINQ query to convert them into SyndicationItems:

var items = (from e in entries
                let itemUri = new Uri(Request.Url, Url.Action("Page", "Wiki", new { page = e.Name }))
                select new
                {
                    Item = new SyndicationItem
                    {
                        Id = itemUri.ToString(),
                        Title = TextSyndicationContent.CreatePlaintextContent(e.Title),
                        Summary = TextSyndicationContent.CreateHtmlContent(
                            _markdown.Render(e.LatestRevision.Body) + String.Format("<img src=\"{0}\" />", itemUri + "/via-feed")),
                        LastUpdatedTime = e.LatestRevision.Revised,
                        Links = 
                        {
                            new SyndicationLink(itemUri) 
                        },
                        Authors = 
                        {
                            new SyndicationPerson { Name = _settings.Author } 
                        },
                    },
                    Keywords = e.MetaKeywords.Split(',')
                }).ToList();

Phew!

You'll notice that this query doesn't actually return a list of SyndicationItem objects. Instead, it returns a list of an anonymous type which has an "Item" property (the SyndicationItem) and a "Keywords" property (a string array). We need to do this, because the very next thing we do is assign those keywords to each item's Categories list:

foreach (var item in items)
{
    foreach (var keyword in item.Keywords)
    {
        item.Item.Categories.Add(new SyndicationCategory(keyword));
    }
}

Pretty straight forward. This means that the feed itself is categorized, which is useful in some feed readers to filter posts by keyword.

Lastly, we call off to a helper method to return the custom ActionResult:

return FeedResult(items.Select(i => i.Item));

Let's have a closer look at that helper method!

private FeedResult FeedResult(IEnumerable<SyndicationItem> items)
{
    return new FeedResult(
        new Atom10FeedFormatter(
            new SyndicationFeed(_settings.SiteTitle, _settings.SearchDescription, new Uri(Request.Url, Url.Action("Recent", "Wiki")), items)
            {
                Id = Request.Url.ToString(),
                Links = 
                { 
                    new SyndicationLink(Request.Url) 
                    { 
                        RelationshipType = "self" 
                    }
                }
            }
        )
    )
    {
        ContentType = "application/atom+xml"
    };
}

Again, it's a bit of an eyeful but it works very nicely. We construct a new SyndicationFeed object with the site's settings, then pass it to a new instance of an Atom10FeedFormatter to create an ATOM 1.0 feed. Lastly, we pass that to our custom FeedResult class which uses the formatter to write the XML out to the response.

The comments feed works exactly the same way, but builds its list of SyndicationItems using properties specific to comments (like the commenter's name) rather than entries.

This change allowed us to remove a lot of code from the project to support feed generation. Removing code is my favourite kind of refactoring!

funnelweb atom feeds asp.net-mvc .net
Posted by: Matt Hamilton
Last revised: 19 Apr, 2024 02:56 PM History

Comments

No comments yet. Be the first!

No new comments are allowed on this post.