Overview
Some of you might have recognised that we are using a customised version of the SubText blogging engine for these blogs. Recently, I've made some changes to support additional feeds and I thought I'd share them with you.
Goals
We wanted to support context-specific feeds throughout the blog site. Since we host multiple blogs in the same site, this meant that we had the following requirements:
- Enable access to the aggregated RSS feed throughout the site
- Enable access to the main blog feed in both Atom and Rss formats
- Enable access to category, day and month feeds from relevant pages
Solution
To address this issue, I found the following:
- The existing Rss.aspx and Atom.aspx pages already supported blog-specific RSS and Atom feeds, but the atom link wasn't being created correctly
- The existing MainFeed.aspx page supported an aggregated RSS feed but wasn't available as a link throughout the site
- A handler existed to support an RSS category-specific feed, but not for day/month archives
My solution comprised two main parts:
- A new ASP.NET user control to implement context-specific feed links on all pages
- A new ASP.NET handler to implement a day-specific and month-specific RSS feed
Part 1: Context-specific feed links
The existing pages already linked to feeds, but these weren't correctly configured. Instead of changing them, I replaced them with a single ASP.NET user control to make it easier to manage. This control had the following markup:
<link runat="server" id="HeaderFeedLinks_Atom" type="application/atom+xml"
title="Blog (Atom)" rel="alternate" visible="false" />
<link runat="server" id="HeaderFeedLinks_RSS" type="application/rss+xml"
title="Blog (Rss)" rel="alternate" visible="false" />
<link runat="server" id="HeaderFeedLinks_Agg" href="/MainFeed.aspx"
type="application/rss+xml" title="Aggregated Feed (RSS)"
rel="alternate" />
<link runat="server" id="HeaderFeedLinks_LocalRSS" type="application/rss+xml"
title="Viewed Content (Rss)" rel="alternate" visible="false" />
As you can see, there isn't much special here except that the links aren't specified in most cases and also that most of the links are not visible by default.
The code-behind for the control determines which links to display and configures them appropriately:
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using Subtext.Web.UI.Pages;
using Subtext.Web.UI.Controls;
using Subtext.Framework;
using System.Text.RegularExpressions;
public partial class HeaderFeedLinks : Subtext.Web.UI.Controls.BaseControl
{
protected void Page_Load(object sender, EventArgs e)
{
string path = Request.Url.AbsolutePath;
/* Looking for different cases as follows
*
* 1. Site root pages - aggregated feed only
* 2. Blog root pages - complete individual feeds
* 2. Category pages - category specific feeds
* 3. Archive summary pages - year, month, day feeds
* 4. Individual posts - complete individual feeds
*
*/
HeaderFeedLinks_Agg.Attributes["href"] =
HttpContext.Current.Request.ApplicationPath + "MainFeed.aspx";
BlogInfo bi = this.CurrentBlog;
if (bi == null) return; // only show the aggregated feed
if (bi.RootUrl.AbsolutePath == HttpContext.Current.Request.ApplicationPath)
{
return; // only show the aggregated feed
}
string baseUrl = bi.RootUrl.AbsolutePath;
if (!path.StartsWith(baseUrl)) return; // unusual path
string relPath = path.Substring(baseUrl.Length);
string feedPath = String.Empty;
bool hasLocal = false;
if (bi.FeedBurnerEnabled && (!String.IsNullOrEmpty(bi.FeedBurnerName)))
{
HeaderFeedLinks_RSS.Visible = true;
HeaderFeedLinks_Atom.Visible = true;
HeaderFeedLinks_RSS.Attributes["href"] =
"http://feeds.feedburner.com/" + bi.FeedBurnerName;
HeaderFeedLinks_Atom.Attributes["href"] =
"http://feeds.feedburner.com/" + bi.FeedBurnerName;
}
else
{
HeaderFeedLinks_RSS.Visible = true;
HeaderFeedLinks_Atom.Visible = true;
HeaderFeedLinks_RSS.Attributes["href"] = baseUrl + "rss.aspx";
HeaderFeedLinks_Atom.Attributes["href"] = baseUrl + "atom.aspx";
}
if (relPath.Length > 0)
{
string ptYear = @"([0-9]{4})";
string ptMonth = @"([0-9]{2})";
string ptDay = @"([0-9]{2})";
string ptFileAndQuery = @"([^?/]+)\.aspx($|\?.*$)";
string ptCat = @"^category";
string ptArchive = @"^archive";
// cases: category, post, day, month, year
string patCategory = ptCat + "/" + ptFileAndQuery;
string patYear = ptArchive + "/" + ptFileAndQuery;
string patMonth = ptArchive + "/" + ptYear + "/" + ptFileAndQuery;
string patDay = ptArchive + "/" + ptYear + "/" + ptMonth + "/"
+ ptFileAndQuery; // -> archive/(1)/(2)/(3)/feed.aspx
string patPost = ptArchive + "/" + ptYear + "/" + ptMonth + "/"
+ ptDay + "/" + ptFileAndQuery;
Regex reCat = new Regex(patCategory, RegexOptions.IgnoreCase);
Regex reYear = new Regex(patYear, RegexOptions.IgnoreCase);
Regex reMonth = new Regex(patMonth, RegexOptions.IgnoreCase);
Regex reDay = new Regex(patDay, RegexOptions.IgnoreCase);
Regex rePost = new Regex(patPost, RegexOptions.IgnoreCase);
if (rePost.IsMatch(relPath))
{
feedPath = String.Empty;
}
else if (reCat.IsMatch(relPath))
{
feedPath = reCat.Replace(relPath, "category/$1.aspx/rss");
hasLocal = true;
}
else if (reYear.IsMatch(relPath)) // not supported by SubText
{
feedPath = reYear.Replace(relPath, "archive/$1/");
}
else if (reMonth.IsMatch(relPath))
{
feedPath = reMonth.Replace(relPath, "archive/$1/$2.aspx/rss");
hasLocal = true;
}
else if (reDay.IsMatch(relPath))
{
feedPath = reDay.Replace(relPath, "archive/$1/$2/$3.aspx/rss");
hasLocal = true;
}
else
{
feedPath = string.Empty;
}
}
if (hasLocal)
{
string absFeedPath = baseUrl + feedPath;
this.HeaderFeedLinks_LocalRSS.Attributes["href"] = absFeedPath;
this.HeaderFeedLinks_LocalRSS.Visible = true;
}
}
}
This control needs to be placed in the DTP.aspx and Default.aspx pages.
Part 2: Day-specific and month-specific RSS feeds
In order to support this, I needed to create a separate class library that included a handler for this output. Having downloaded the source for SubText, I decided to create a modified copy of the Subtext.Framework.Syndication.RssHandler class. The complete code is listed below:
#region Disclaimer/Info
///////////////////////////////////////////////////////////////////////////////
// Subtext WebLog
//
// Subtext is an open source weblog system that is a fork of the .TEXT
// weblog system.
//
// For updated news and information please visit http://subtextproject.com/
// Subtext is hosted at SourceForge at http://sourceforge.net/projects/subtext
// The development mailing list is at subtext-devs@lists.sourceforge.net
//
// This project is licensed under the BSD license. See the License.txt file
// for more information.
///////////////////////////////////////////////////////////////////////////////
#endregion
using System;
using System.Collections.Generic;
using Subtext.Framework.Data;
using Subtext.Framework;
using Subtext.Framework.Components;
using Subtext.Framework.Syndication;
using Subtext.Framework.Util;
using System.Text.RegularExpressions;
namespace Subtext.Framework.Syndication
{
/// <summary>
/// Class used to handle requests for an RSS feed.
/// </summary>
public class RssMonthHandler :
Subtext.Framework.Syndication.BaseSyndicationHandler<Entry>
{
BaseSyndicationWriter<Entry> writer;
#region DayMonth support
DateTime _MonthDay = DateTime.MinValue;
private bool _isMonth; // only set after Month is accessed
protected DateTime MonthDay
{
get
{
if (_MonthDay != DateTime.MinValue) return _MonthDay;
string ptYear = @"([0-9]{4})";
string ptMonth = @"([0-9]{2})";
string ptFileAndQuery = @"([^?/]+)\.aspx/rss($|\?.*$)";
string ptArchive = @"^archive";
string patMonth = ptArchive + "/" + ptYear + "/" + ptFileAndQuery;
string patDay = ptArchive + "/" + ptYear + "/" + ptMonth + "/" +
ptFileAndQuery;
Regex reMonth = new Regex(patMonth, RegexOptions.IgnoreCase);
Regex reDay = new Regex(patDay, RegexOptions.IgnoreCase);
string blogRoot = this.CurrentBlog.RootUrl.AbsolutePath;
string reqPath = this.Context.Request.Url.AbsolutePath;
string relPath = reqPath.Substring(blogRoot.Length);
if (reDay.IsMatch(relPath))
{
Match match = reDay.Match(relPath);
_isMonth = false;
_MonthDay = new DateTime(
int.Parse(match.Groups[1].Value),
int.Parse(match.Groups[2].Value),
int.Parse(match.Groups[3].Value));
}
else if (reMonth.IsMatch(relPath))
{
Match match = reMonth.Match(relPath);
_isMonth = true;
_MonthDay = new DateTime(
int.Parse(match.Groups[1].Value),
int.Parse(match.Groups[2].Value),
1);
}
else
{
_MonthDay = DateTime.Today;
_isMonth = true; // present the current month's posts
}
return _MonthDay;
}
}
#endregion
/// <summary>
/// Returns the key used to cache this feed.
/// </summary>
/// <param name="dateLastViewedFeedItemPublished">Date last viewed feed
/// item published.</param>
/// <returns></returns>
protected override string CacheKey(DateTime dateLastViewedFeedItemPublished)
{
const string key =
"RSS;IndividualArchiveFeed;BlogId:{0};LastViewed:{1};" +
"Year:{2};Month:{3};Day:{4}";
DateTime monthday = this.MonthDay;
return string.Format(key, CurrentBlog.Id,
dateLastViewedFeedItemPublished,
monthday.Year, monthday.Month ,
_isMonth ? "ALL" : monthday.Day.ToString());
}
/// <summary>
/// Caches the specified RSS feed.
/// </summary>
/// <param name="feed">Feed.</param>
protected override void Cache(CachedFeed feed)
{
Context.Cache.Insert(CacheKey(
this.SyndicationWriter.DateLastViewedFeedItemPublished),
feed, null, DateTime.Now.AddSeconds((double)CacheDuration.Medium),
TimeSpan.Zero);
}
protected DateTime GetLatestPostStamp(IList<Entry> posts)
{
DateTime stamp = DateTime.MinValue;
foreach (Entry e in posts)
{
if (e.DateCreated > stamp) stamp = e.DateCreated;
}
return stamp;
}
/// <summary>
/// Gets the syndication writer.
/// </summary>
/// <returns></returns>
protected override BaseSyndicationWriter<Entry> SyndicationWriter
{
get
{
if (writer == null)
{
DateTime monthday = this.MonthDay;
IList<Entry> posts = _isMonth ?
Entries.GetPostCollectionByMonth(
monthday.Month, monthday.Year) :
Entries.GetSingleDay(monthday);
DateTime stamp = GetLatestPostStamp(posts);
writer = new RssWriter(posts, stamp, this.UseDeltaEncoding);
}
return writer;
}
}
/// <summary>
/// Returns true if the feed is the main feed.
/// False for category feeds and comment feeds.
/// </summary>
protected override bool IsMainfeed
{
get { return false; }
}
}
}
All that remains is to link the handler into the web.config:
<HttpHandler pattern="(?:/archive/\d{4}/\d{2}.aspx/rss)$"
type="Subtext.Framework.Syndication.RssMonthHandler, HandlerDll"
handlerType="Direct" />
<HttpHandler pattern="(?:/archive/\d{4}/\d{2}/\d{2}.aspx/rss)$"
type="Subtext.Framework.Syndication.RssMonthHandler, HandlerDll"
handlerType="Direct" />
Note that these handlers go into the SubText configuration, not the standard ASP.NET handler configuration.
Versions
Metadata
- Categories: SubText, blogging, Software Development, ASP.NET, .NET
- Additional keywords: SubText API, RSS, Atom
- Technorati Tags: ASP.NET, .NET, software development, SubText, SubText API, blogging