Atom Feeds of Comments in Blosxom

January 16, 2008

I use Frank Hecker’s feedback plugin to handle comments and trackbacks on this site. It’s a great plugin with built-in spam protection, moderation, support for templates, etc. I’ve been thinking of writing a Blosxom plugin to generate atom feeds from the comments for a while now.

I realized today that this wasn’t nearly as difficult as I first thought. I was able to quickly get this working by creating a new flavour (I called it .cmt for “comments”) and adding a few things to the feedback plugin. You can see this in action now in my Atom feed. I’ll describe how it all works in case someone else wants to implement this (for Blosxom or perhaps even another program). This requires a little background knowledge of the Atom specification, Blosxom’s template and plugin systems, and a copy of the feedback plugin.

We need to generate a single <entry> item in the feed for each comment. Blosxom’s story template is not sufficient because there are multiple comments for each story. However, the feedback plugin allows one to use a comment template. So if one creates the proper head and foot templates and then generates the Atom <entry> items in the comment template, it is possible to construct a valid feed. There, of course, a few details of the specification that require a bit of additional Perl code. Namely, the <updated> dates must be in a specific format and one needs to keep track of the <updated> date of the feed as a whole while processing the comments.

First, I will describe the new flavour, using variables that the feedback plugin doesn’t yet provide, and then I’ll outline the required modifications to the feedback plugin.

One important point is that each comment body needs to either be formatted as valid XHTML or sent as escaped HTML tag soup. I assume the former and this is easily accomplished by asking the feedback plugin to use Markdown to format your comments (my $comment_format = 'markdown';). Otherwise, you need to escape the HTML somehow (perhaps using the atomfeed plugin’s escape routine).

The Templates

The head.cmt template looks like this:

<?xml version="1.0" encoding="iso-8859-1"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="$url">
<title>$blog_title: Comments</title>
  <link rel="self" type="application/atom+xml" href="$url$feedback::path" />
  <link rel="alternate" type="application/xhtml+xml" href="$url$feedback::basename.$default_flavour" />
  <id>$url$feedback::path</id>
  <generator uri="http://blosxom.sourceforge.net/" version="$version">Blosxom</generator>
  <author>
    <name>Jason Blevins</name>
    <email>jrblevin@sdf.lonestar.org</email>
    <uri>https://jblevins.org</uri>
  </author>
  <icon>https://jblevins.org/favicon.ico</icon>
  {{updated}}

There are a couple of things to note here. First, the feedback plugin does not provide $feedback::path or feedback::basename. Code to generate these variables will be added later. Second, we use a placeholder, {{updated}}, for the <updated> item. We will use the date of the most recent comment here. This tag must appear before all entries, but we cannot know its value before we process the comments. Thus, we use the placeholder text and replace it in the foot() subroutine.

Now, we need to generate a single <entry> for each comment. In the story.cmt template, simply put

$feedback::comments

We let the feedback plugin handle generating each <entry> item by placing the following in the comment.cmt template:

  <entry>
    <id>$url$path/$fn#$feedback::id</id>
    <link rel="alternate" type="text/html" href="$url$path/$fn.$default_flavour" />
    <title>$feedback::name comments on $title</title>
    <published>$feedback::utc_date</published>
    <updated>$feedback::utc_date</updated>
    <author>
      <name>$feedback::name</name>
    </author>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
$feedback::comment
      </div>
    </content>
  </entry>

I’ve used the interpolate_fancy plugin here to test for $feedback::url. Note that $feedback::id does not exist yet but we will eventually add code which generates the ID using the UNIX time when the comment was submitted. This is stored by the feedback plugin already. Other self explanatory variables that will need to be created are $feedback::name, $feedback::url, and $feedback::utc_date.

This approach will provide both an /index.cmt feed, as well as story-specific /category/story-title.cmt comment feeds. The former can be used as a site-wide comments-only feed, although it will only contain comments for the first $blosxom::num_entries stories.1 The latter story-specific comment feeds can be linked to from your primary site-wide Atom feed to provide a dynamically-updated list of comments for each story (see below).

Modifying the feedback Plugin

First we define the new template variables by adding the following to use vars near the top of the feedback plugin:

$path $basename $flavour $utc_date $latest_utc_date $id $host $url

and define the following somewhere near the top of the file:

# Comment feed flavour
my $flavour = 'cmt';

# <updated> tag placeholder text
my $template_placeholder = '{{updated}}';

# Timestamp of the latest comment (of those that are processed)
my $latest_date = 0;

In start(), by default the feedback plugin only reads comments for story pages. We want it to also run when the flavour is cmt. Change the following condition (line 339 of version 0.23):

if ($is_story_page) {
    ($comments, $comments_count, $trackbacks, $trackbacks_count) =
        get_feedback($path);
}

to

if (($is_story_page) || ($blosxom::flavour eq $flavour))

Add a head() subroutine to make these variables available to the template:

sub head {
    $path = path_info() || param('path');
    $basename = $path;
    $basename =~ s!\.$flavour$!!;
}

That is, $path contains the full path of the feed, including the extension (e.g. /category/story-title.cmt). We use this in the comment feed’s <id> tag, it’s rel="self" link, and optionally in the main feed’s rel="replies" link.

Add the following subroutine to the end of the file. It returns a date in the format required by the Atom specification:

sub date_to_utc {
    my $time = shift;
    my @utc = gmtime($time);
    return sprintf("%4d-%02d-%02dT%02d:%02d:%02dZ",
           $utc[5]+1900, $utc[4]+1, $utc[3], $utc[2], $utc[1], $utc[0]);
}

We need to define a few more variables used in the comment.cmt template. In sub get_feedback, in the if ($param{'comment'}) block (line 513 in version 0.23), add the following:

$id = $param{'date'};
$url = $param{'url'};
$utc_date = date_to_utc($param{'date'});
if ($param{'date'} > $latest_date) {
    $latest_date = $param{'date'};
    $latest_utc_date = date_to_utc($latest_date);
}

These lines define a (most likely) unique comment ID using the time in seconds (I also use this in the comment.html template to create an anchor, as in id="$feedback::id"), the URL of the comment author, the formatted date of the post. It also keeps track of the most recent comment (used for the <updated> tag in the feed).

Finally, we replace the placeholder text in the in the foot() subroutine:

sub foot {
    my($pkg, $currentdir, $foot_ref) = @_;
    # Replace the placeholder with the feed-level <updated> element:
    my $feed_utc_date = "<updated>$feedback::latest_utc_date</updated>";
    $blosxom::output =~ s/$template_placeholder/$feed_utc_date/m;
    return 1;
}

Atom Threading Extensions

If you have also provide an Atom feed of your stories, you can use the Atom Threading Extensions to provide a list of comments in the same feed, below each story. First of all, you need to declare the thread namespace in your head.atom template:

<?xml version="1.0" encoding="iso-8859-1"?>
<feed xmlns="http://www.w3.org/2005/Atom"
      xml:base="http://$atomfeed::id_domain"
      xmlns:thr="http://purl.org/syndication/thread/1.0">

Then for each entry, provide a link to the relevant comment feed in the story.atom template:

<link rel="replies" type="application/atom+xml" href="$url$path/$fn.$feedback::flavour" />

It is recommended2 to also add a corresponding in-reply-to link in the comment feed, in the comment.cmt template:

<thr:in-reply-to
     ref="tag$atomfeed::colon$atomfeed::id_domain,$atomfeed::utc_yr$atomfeed::colon$path/$fn"
     type="text/html"
     href="$url$path/$fn.$default_flavour"/>

You need to use the correct ref tag, the id of the original story in the entries atom feed. This can be found in your story.atom template. If you use the default template, the above string will be correct. If you do this you also need to add the xmlns:thr="http://purl.org/syndication/thread/1.0" bit to head.cmt.


  1. You can increase this by using the config plugin and setting $blosxom::num_entries = 999; in the config.cmt file. Still, this is not an optimal solution.

  2. The original page, http://www.snellspace.com/wp/?p=691, is no longer available as of June 17, 2010.