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
.