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:

  1. A new ASP.NET user control to implement context-specific feed links on all pages
  2. 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


Bookmark with :
Digg It! DZone StumbleUpon Technorati Reddit Del.icio.us Newsvine Furl Blinklist
posted @ Tuesday, May 06, 2008 1:47 PM | in .NET ASP.NET Software Development

Comments

No comments posted yet.

Post Comment

Title *
Name *
Email
Url
Comment *  


Please add 8 and 5 and type the answer here: