A year ago, I wrote a scary post about RSS in Orchard. RSS was one of the first features we implemented in our CMS, and it has stood the test of time rather well, but the post was explaining things at a level that was probably too abstract whereas my readers were expecting something a little more practical. Well, this post is going to correct this by showing how I built a module that adds RSS feeds for each tag on the site. Hopefully it will show that it's not very complicated in practice, and also that the infrastructure is pretty well thought out. In order to provide RSS, we need to do two things: generate the XML for the feed, and inject the address of that feed into the existing tag listing page, in order to make the feed discoverable. Let's start with the discoverability part. One might be tempted to replace the controller or the view that are responsible for the listing of the items under a tag. Fortunately, there is no need to do any of that, and we can be a lot less obtrusive. Instead, we can implement a filter: public class TagRssFilter : FilterProvider, IResultFilter
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
On this filter, we can implement the OnResultExecuting method and simply check whether the current request is targeting the list of items under a tag. If that is the case, we can just register our new feed:
public void OnResultExecuting(ResultExecutingContext filterContext) {
var routeValues = filterContext.RouteData.Values;
if (routeValues["area"] as string == "Orchard.Tags" &&
routeValues["controller"] as string == "Home" &&
routeValues["action"] as string == "Search") {
var tag = routeValues["tagName"] as string;
if (! string.IsNullOrWhiteSpace(tag)) {
var workContext = _wca.GetContext();
_feedManager.Register(
workContext.CurrentSite + " – " + tag,
"rss",
new RouteValueDictionary { { "tag", tag } } );
}
}
}
The registration of the new feed is just specifying the title of the feed, its format (RSS) and the parameters that it will need (the tag). _wca and _feedManager are just instances of IWorkContextAccessor and IFeedManager that Orchard injected for us. That is all that's needed to get the following tag to be added to the head of our page, without touching an existing controller or view:
<link rel="alternate" type="application/rss+xml" title="VuLu - Science" href="/rss?tag=Science"/>
Nifty.
Of course, if we navigate to the URL of that feed, we'll get a 404. This is because no implementation of IFeedQueryProvider knows about the tag parameter yet. Let's build one that does:
public class TagFeedQuery : IFeedQueryProvider, IFeedQuery
IFeedQueryProvider has one method, Match, that we can implement to take over any feed request that has a tag parameter:
public FeedQueryMatch Match(FeedContext context) {
var tagName = context.ValueProvider.GetValue("tag");
if (tagName == null) return null;
return new FeedQueryMatch { FeedQuery = this, Priority = -5 };
}
This is just saying that if there is a tag parameter, we will handle it.
All that remains to be done is the actual building of the feed now that we have accepted to handle it. This is done by implementing the Execute method of the IFeedQuery interface:
public void Execute(FeedContext context) {
var tagValue = context.ValueProvider.GetValue("tag");
if (tagValue == null) return;
var tagName = (string)tagValue.ConvertTo(typeof (string));
var tag = _tagService.GetTagByName(tagName);
if (tag == null) return;
var site = _services.WorkContext.CurrentSite;
var link = new XElement("link");
context.Response.Element.SetElementValue("title", site.SiteName + " - " + tagName);
context.Response.Element.Add(link);
context.Response.Element.SetElementValue("description", site.SiteName + " - " + tagName);
context.Response.Contextualize(requestContext => link.Add(GetTagUrl(tagName, requestContext)));
var items = _tagService.GetTaggedContentItems(tag.Id, 0, 20);
foreach (var item in items) {
context.Builder.AddItem(context, item.ContentItem);
}
}
This code is resolving the tag content item from its name and then gets content items tagged with it, using the tag services provided by the Orchard.Tags module. Then we add those items to the feed.
And that is it. To summarize, we handled the request unobtrusively in order to inject the feed's link, then handled requests for feeds with a tag parameter and generated the list of items for that tag. It remains fairly simple and still it is able to handle arbitrary content types. That makes me quite happy about our little piece of over-engineered code from last year.
The full code for this can be found in the Vandelay.TagCloud module:
http://orchardproject.net/gallery/List/Modules/
Orchard.Module.Vandelay.TagCloud/1.2