4 # Author: Rael Dornfest (2002-2003), The Blosxom Development Team (2005-2008)
5 # Version: 2.1.2 ($Id: blosxom.cgi,v 1.91 2009/03/08 00:58:52 xtaran Exp $)
6 # Home/Docs/Licensing: http://blosxom.sourceforge.net/
7 # Development/Downloads: http://sourceforge.net/projects/blosxom
13 blosxom - A lightweight yet feature-packed weblog
17 B<blosxom> is a simple web log (blog) CGI script written in perl.
21 B<Blosxom> (pronounced "I<blossom>") is a lightweight yet feature-packed
22 weblog application designed from the ground up with simplicity,
23 usability, and interoperability in mind.
25 Fundamental is its reliance upon the file system, folders and files
26 as its content database. Blosxom's weblog entries are plain text
27 files like any other. Write from the comfort of your favorite text
28 editor and hit the Save button. Create, edit, rename, and delete entries
29 on the command-line, via FTP, WebDAV, or anything else you
30 might use to manipulate your files. There's no import or export; entries
31 are nothing more complex than title on the first line, body being
32 everything thereafter.
34 Despite its tiny footprint, Blosxom doesn't skimp on features, sporting
35 the majority of features one would find in any other Weblog application.
37 Blosxom is simple, straightforward, minimalist Perl affording even the
38 dabbler an opportunity for experimentation and customization. And
39 last, but not least, Blosxom is open source and free for the taking and
44 Write a weblog entry, and place it into the main data directory. Place
45 the the title is on the first line; the body is everything afterwards.
46 For example, create a file named I<first.txt> and put in it something
51 I have successfully installed blosxom on this system. For more
52 information on blosxom, see the author's <a
53 href="http://blosxom.sourceforge.net/">blosxom site</a>.
55 Place the file in the directory under the I<$datadir> points to. Be
56 sure to change the default location to be somewhere accessable by the
57 web server that runs blosxom as a CGI program.
61 # --- Configurable variables -----
63 # What's this blog's title?
64 $blog_title = "My Weblog";
66 # What's this blog's description (for outgoing RSS feed)?
67 $blog_description = "Yet another Blosxom weblog.";
69 # What's this blog's primary language (for outgoing RSS feed)?
70 $blog_language = "en";
72 # What's this blog's text encoding ?
73 $blog_encoding = "UTF-8";
75 # Where are this blog's entries kept?
76 $datadir = "/Library/WebServer/Documents/blosxom";
78 # What's my preferred base URL for this blog (leave blank for
82 # Should I stick only to the datadir for items or travel down the
83 # directory hierarchy looking for items? If so, to what depth?
85 # 0 = infinite depth (aka grab everything), 1 = datadir only,
90 # How many entries should I show on the home page?
93 # What file extension signifies a blosxom entry?
94 $file_extension = "txt";
96 # What is the default flavour?
97 $default_flavour = "html";
99 # Should I show entries from the future (i.e. dated after now)?
100 $show_future_entries = 0;
102 # --- Plugins (Optional) -----
104 # File listing plugins blosxom should load (if empty blosxom will load
105 # all plugins in $plugin_dir and $plugin_path directories)
108 # Where are my plugins kept?
111 # Where should my plugins keep their state information?
112 $plugin_state_dir = "$plugin_dir/state";
114 # Additional plugins location. A list of directories, separated by ';'
115 # on windows, ':' everywhere else.
118 # --- Static Rendering -----
120 # Where are this blog's static files to be created?
121 $static_dir = "/Library/WebServer/Documents/blog";
123 # What's my administrative password (you must set this for static
125 $static_password = "";
127 # What flavours should I generate statically?
128 @static_flavours = qw/html rss/;
130 # Should I statically generate individual entries?
134 # --- Advanced Encoding Options -----
136 # Should I encode entities for xml content-types? (plugins can turn
137 # this off if they do it themselves)
138 $encode_xml_entities = 1;
140 # --------------------------------
146 =item B<BLOSXOM_CONFIG_FILE>
148 Points to the location of the configuration file. This will be
149 considered as first option, if it's set.
152 =item B<BLOSXOM_CONFIG_DIR>
154 The here named directory will be tried unless the above mentioned
155 environment variable is set and tested for a contained blosxom.conf
166 =item B</usr/lib/cgi-bin/blosxom>
168 The CGI script itself. Please note that the location might depend on
171 =item B</etc/blosxom/blosxom.conf>
173 The default configuration file location. This is rather taken as last
174 ressort if no other configuration location is set through environment
182 Rael Dornfest <rael@oreilly.com> was the original author of blosxom. The
183 development was picked up by a team of dedicated users of blosxom since
184 2005. See <I<http://blosxom.sourceforge.net/>> for more information.
190 qw! $version $blog_title $blog_description $blog_language $blog_encoding $datadir $url %template $template $depth $num_entries $file_extension $default_flavour $static_or_dynamic $config_dir $plugin_list $plugin_path $plugin_dir $plugin_state_dir @plugins %plugins $static_dir $static_password @static_flavours $static_entries $path_info_full $path_info $path_info_yr $path_info_mo $path_info_da $path_info_mo_num $flavour $static_or_dynamic %month2num @num2month $interpolate $entries $output $header $show_future_entries %files %indexes %others $encode_xml_entities $content_type !;
197 use CGI qw/:standard :netscape/;
199 $version = "2.1.2+dev";
201 # Load configuration from $ENV{BLOSXOM_CONFIG_DIR}/blosxom.conf, if it exists
203 if ( $ENV{BLOSXOM_CONFIG_FILE} && -r $ENV{BLOSXOM_CONFIG_FILE} ) {
204 $blosxom_config = $ENV{BLOSXOM_CONFIG_FILE};
205 ( $config_dir = $blosxom_config ) =~ s! / [^/]* $ !!x;
208 for my $blosxom_config_dir ( $ENV{BLOSXOM_CONFIG_DIR}, '/etc/blosxom',
211 if ( -r "$blosxom_config_dir/blosxom.conf" ) {
212 $config_dir = $blosxom_config_dir;
213 $blosxom_config = "$blosxom_config_dir/blosxom.conf";
219 # Load $blosxom_config
220 if ($blosxom_config) {
221 if ( -r $blosxom_config ) {
222 eval { require $blosxom_config }
223 or warn "Error reading blosxom config file '$blosxom_config'"
224 . ( $@ ? ": $@" : '' );
227 warn "Cannot find or read blosxom config file '$blosxom_config'";
231 my $fh = new FileHandle;
248 @num2month = sort { $month2num{$a} <=> $month2num{$b} } keys %month2num;
250 # Use the stated preferred URL or figure it out automatically. Set
251 # $url manually in the config section above if CGI.pm doesn't guess
252 # the base URL correctly, e.g. when called from a Server Side Includes
257 # Unescape %XX hex codes (from URI::Escape::uri_unescape)
258 $url =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
260 # Support being called from inside a SSI document
261 $url =~ s/^included:/http:/ if $ENV{SERVER_PROTOCOL} eq 'INCLUDED';
263 # Remove PATH_INFO if it is set but not removed by CGI.pm. This
264 # seems to happen when used with Apache's Alias directive or if
265 # called from inside a Server Side Include document. If that
266 # doesn't help either, set $url manually in the configuration.
267 $url =~ s/\Q$ENV{PATH_INFO}\E$// if defined $ENV{PATH_INFO};
271 # There is one case where this code does more than necessary, too:
272 # If the URL requested is e.g. http://example.org/blog/blog and
273 # the base URL is correctly determined as http://example.org/blog
274 # by CGI.pm, then this code will incorrectly normalize the base
275 # URL down to http://example.org, because the same string as
276 # PATH_INFO is part of the base URL, too. But this is such a
277 # seldom case and can be fixed by setting $url in the config file,
281 # The only modification done to a manually set base URL is to strip
282 # a trailing slash if present.
286 # Drop ending any / from dir settings
288 $plugin_dir =~ s!/$!!;
289 $static_dir =~ s!/$!!;
291 # Fix depth to take into account datadir's path
292 $depth += ( $datadir =~ tr[/][] ) - 1 if $depth;
294 if ( !$ENV{GATEWAY_INTERFACE}
295 and param('-password')
297 and param('-password') eq $static_password )
299 $static_or_dynamic = 'static';
302 $static_or_dynamic = 'dynamic';
303 param( -name => '-quiet', -value => 1 );
307 # Take a gander at HTTP's PATH_INFO for optional blog name, archive yr/mo/day
308 my @path_info = split m{/}, path_info() || param('path');
309 $path_info_full = join '/', @path_info; # Equivalent to $ENV{PATH_INFO}
312 # Flavour specified by ?flav={flav} or index.{flav}
314 if (! ($flavour = param('flav'))) {
315 if ( $path_info[$#path_info] =~ /(.+)\.(.+)$/ ) {
317 pop @path_info if $1 eq 'index';
320 $flavour ||= $default_flavour;
322 # Fix XSS in flavour name (CVE-2008-2236)
323 $flavour = blosxom_html_escape($flavour);
325 sub blosxom_html_escape {
334 my $escape_re = join '|' => keys %escape;
335 $string =~ s/($escape_re)/$escape{$1}/g;
339 # Global variable to be used in head/foot.{flavour} templates
341 # Add all @path_info elements to $path_info till we come to one that could be a year
342 while ( $path_info[0] && $path_info[0] !~ /^(19|20)\d{2}$/) {
343 $path_info .= '/' . shift @path_info;
346 # Pull date elements out of path
347 if ($path_info[0] && $path_info[0] =~ /^(19|20)\d{2}$/) {
348 $path_info_yr = shift @path_info;
350 ($path_info[0] =~ /^(0\d|1[012])$/ ||
351 exists $month2num{ ucfirst lc $path_info_mo })) {
352 $path_info_mo = shift @path_info;
353 # Map path_info_mo to numeric $path_info_mo_num
354 $path_info_mo_num = $path_info_mo =~ /^\d{2}$/
356 : $month2num{ ucfirst lc $path_info_mo };
357 if ($path_info[0] && $path_info[0] =~ /^[0123]\d$/) {
358 $path_info_da = shift @path_info;
363 # Add remaining path elements to $path_info
364 $path_info .= '/' . join('/', @path_info);
366 # Strip spurious slashes
367 $path_info =~ s!(^/*)|(/*$)!!g;
369 # Define standard template subroutine, plugin-overridable at Plugins: Template
371 my ( $path, $chunk, $flavour ) = @_;
374 return join '', <$fh>
375 if $fh->open("< $datadir/$path/$chunk.$flavour");
376 } while ( $path =~ s/(\/*[^\/]*)$// and $1 );
378 # Check for definedness, since flavour can be the empty string
379 if ( defined $template{$flavour}{$chunk} ) {
380 return $template{$flavour}{$chunk};
382 elsif ( defined $template{error}{$chunk} ) {
383 return $template{error}{$chunk};
390 # Bring in the templates
393 last if /^(__END__)$/;
394 my ( $ct, $comp, $txt ) = /^(\S+)\s(\S+)(?:\s(.*))?$/ or next;
396 $template{$ct}{$comp} .= $txt . "\n";
400 my $path_sep = $^O eq 'MSWin32' ? ';' : ':';
401 my @plugin_dirs = split /$path_sep/, $plugin_path;
402 unshift @plugin_dirs, $plugin_dir;
403 my @plugin_list = ();
404 my %plugin_hash = ();
406 # If $plugin_list is set, read plugins to use from that file
407 if ( $plugin_list ) {
408 if ( -r $plugin_list and $fh->open("< $plugin_list") ) {
409 @plugin_list = map { chomp $_; $_ } grep { /\S/ && !/^#/ } <$fh>;
413 warn "unable to read or open plugin_list '$plugin_list': $!";
418 # Otherwise walk @plugin_dirs to get list of plugins to use
419 if ( ! @plugin_list && @plugin_dirs ) {
420 for my $plugin_dir (@plugin_dirs) {
421 next unless -d $plugin_dir;
422 if ( opendir PLUGINS, $plugin_dir ) {
424 grep { /^[\w:]+$/ && !/~$/ && -f "$plugin_dir/$_" }
429 next if $plugin_hash{$plugin};
431 # Add to @plugin_list and %plugin_hash
432 $plugin_hash{$plugin} = "$plugin_dir/$plugin";
433 push @plugin_list, $plugin;
438 @plugin_list = sort @plugin_list;
441 # Load all plugins in @plugin_list
442 unshift @INC, @plugin_dirs;
443 foreach my $plugin (@plugin_list) {
444 my ( $plugin_name, $off ) = $plugin =~ /^\d*([\w:]+?)(_?)$/;
445 my $plugin_file = $plugin_list ? $plugin_name : $plugin;
446 my $on_off = $off eq '_' ? -1 : 1;
448 # Allow perl module plugins
449 # The -z test is a hack to allow a zero-length placeholder file in a
450 # $plugin_path directory to indicate an @INC module should be loaded
451 if ( $plugin =~ m/::/ && ( $plugin_list || -z $plugin_hash{$plugin} ) ) {
453 # For Blosxom::Plugin::Foo style plugins, we need to use a string require
454 eval "require $plugin_file";
457 { # we try first to load from $plugin_dir before attempting from $plugin_path
458 eval { require "$plugin_dir/$plugin_file" }
459 or eval { require $plugin_file };
463 warn "error finding or loading blosxom plugin '$plugin_name': $@";
466 if ( $plugin_name->start() and ( $plugins{$plugin_name} = $on_off ) ) {
467 push @plugins, $plugin_name;
471 shift @INC foreach @plugin_dirs;
474 # Allow for the first encountered plugin::template subroutine to override the
475 # default built-in template subroutine
476 foreach my $plugin (@plugins) {
477 if ( $plugins{$plugin} > 0 and $plugin->can('template') ) {
478 if ( my $tmp = $plugin->template() ) {
485 # Provide backward compatibility for Blosxom < 2.0rc1 plug-ins
487 return &$template(@_);
490 # Define default entries subroutine
492 my ( %files, %indexes, %others );
496 my $curr_depth = $File::Find::dir =~ tr[/][];
497 return if $depth and $curr_depth > $depth;
503 =~ m!^$datadir/(?:(.*)/)?(.+)\.$file_extension$!
505 # not an index, .file, and is readable
506 and $2 ne 'index' and $2 !~ /^\./ and ( -r $File::Find::name )
510 # read modification time
511 my $mtime = stat($File::Find::name)->mtime or return;
513 # to show or not to show future entries
514 return unless ( $show_future_entries or $mtime < time );
516 # add the file and its associated mtime to the list of files
517 $files{$File::Find::name} = $mtime;
519 # static rendering bits
521 = "$static_dir/$1/index." . $static_flavours[0];
524 or stat($static_file)->mtime < $mtime )
527 $d = join( '/', ( nice_date($mtime) )[ 5, 2, 3 ] );
529 $indexes{ ( $1 ? "$1/" : '' ) . "$2.$file_extension" } = 1
534 # not an entries match
535 elsif ( !-d $File::Find::name and -r $File::Find::name ) {
536 $others{$File::Find::name} = stat($File::Find::name)->mtime;
542 return ( \%files, \%indexes, \%others );
546 # Allow for the first encountered plugin::entries subroutine to override the
547 # default built-in entries subroutine
548 foreach my $plugin (@plugins) {
549 if ( $plugins{$plugin} > 0 and $plugin->can('entries') ) {
550 if ( my $tmp = $plugin->entries() ) {
557 my ( $files, $indexes, $others ) = &$entries();
558 %indexes = %$indexes;
561 if ( !$ENV{GATEWAY_INTERFACE}
562 and param('-password')
564 and param('-password') eq $static_password )
567 param('-quiet') or print "Blosxom is generating static index pages...\n";
569 # Home Page and Directory Indexes
571 foreach my $path ( sort keys %indexes ) {
573 foreach ( ( '', split /\//, $path ) ) {
577 mkdir "$static_dir/$p", 0755
578 unless ( -d "$static_dir/$p" or $p =~ /\.$file_extension$/ );
579 foreach $flavour (@static_flavours) {
581 = ( &$template( $p, 'content_type', $flavour ) );
582 $content_type =~ s!\n.*!!s;
583 my $fn = $p =~ m!^(.+)\.$file_extension$! ? $1 : "$p/index";
584 param('-quiet') or print "$fn.$flavour\n";
585 my $fh_w = new FileHandle "> $static_dir/$fn.$flavour"
586 or die "Couldn't open $static_dir/$p for writing: $!";
588 if ( $indexes{$path} == 1 ) {
594 $path_info =~ s!\.$file_extension$!\.$flavour!;
595 print $fh_w &generate( 'static', $path_info, '', $flavour,
602 $path_info_yr, $path_info_mo,
603 $path_info_da, $path_info
604 ) = split /\//, $p, 4;
605 unless ( defined $path_info ) { $path_info = "" }
606 print $fh_w &generate( 'static', '', $p, $flavour,
617 $content_type = ( &$template( $path_info, 'content_type', $flavour ) );
618 $content_type =~ s!\n.*!!s;
620 $content_type =~ s/(\$\w+(?:::\w+)*)/"defined $1 ? $1 : ''"/gee;
621 $header = { -type => $content_type };
623 print generate( 'dynamic', $path_info,
624 "$path_info_yr/$path_info_mo_num/$path_info_da",
625 $flavour, $content_type );
629 foreach my $plugin (@plugins) {
630 if ( $plugins{$plugin} > 0 and $plugin->can('end') ) {
631 $entries = $plugin->end();
637 my ( $static_or_dynamic, $currentdir, $date, $flavour, $content_type )
641 %others = ref $others ? %$others : ();
644 foreach my $plugin (@plugins) {
645 if ( $plugins{$plugin} > 0 and $plugin->can('filter') ) {
646 $entries = $plugin->filter( \%files, \%others );
653 # Allow plugins to decide if we can cut short story generation
655 foreach my $plugin (@plugins) {
656 if ( $plugins{$plugin} > 0 and $plugin->can('skip') ) {
657 if ( my $tmp = $plugin->skip() ) {
664 # Define default interpolation subroutine
667 my $template = shift;
668 # Interpolate scalars, namespaced scalars, and hash/hashref scalars
669 $template =~ s/(\$\w+(?:::\w+)*(?:(?:->)?{(['"]?)[-\w]+\2})?)/"defined $1 ? $1 : ''"/gee;
673 unless ( defined($skip) and $skip ) {
675 # Plugins: Interpolate
676 # Allow for the first encountered plugin::interpolate subroutine to
677 # override the default built-in interpolate subroutine
678 foreach my $plugin (@plugins) {
679 if ( $plugins{$plugin} > 0 and $plugin->can('interpolate') ) {
680 if ( my $tmp = $plugin->interpolate() ) {
688 my $head = ( &$template( $currentdir, 'head', $flavour ) );
691 foreach my $plugin (@plugins) {
692 if ( $plugins{$plugin} > 0 and $plugin->can('head') ) {
693 $entries = $plugin->head( $currentdir, \$head );
697 $head = &$interpolate($head);
703 my $ne = $num_entries;
705 if ( $currentdir =~ /(.*?)([^\/]+)\.(.+)$/ and $2 ne 'index' ) {
706 $currentdir = "$1$2.$file_extension";
707 %f = ( "$datadir/$currentdir" => $files{"$datadir/$currentdir"} )
708 if $files{"$datadir/$currentdir"};
711 $currentdir =~ s!/index\..+$!!;
714 # Define a default sort subroutine
716 my ($files_ref) = @_;
718 sort { $files_ref->{$b} <=> $files_ref->{$a} }
723 # Allow for the first encountered plugin::sort subroutine to override the
724 # default built-in sort subroutine
725 foreach my $plugin (@plugins) {
726 if ( $plugins{$plugin} > 0 and $plugin->can('sort') ) {
727 if ( my $tmp = $plugin->sort() ) {
734 foreach my $path_file ( &$sort( \%f, \%others ) ) {
735 last if $ne <= 0 && $date !~ /\d/;
736 use vars qw/ $path $fn /;
738 = $path_file =~ m!^$datadir/(?:(.*)/)?(.*)\.$file_extension!;
740 # Only stories in the right hierarchy
741 $path =~ /^$currentdir/
742 or $path_file eq "$datadir/$currentdir"
745 # Prepend a slash for use in templates only if a path exists
748 # Date fiddling for by-{year,month,day} archive views
750 qw/ $dw $mo $mo_num $da $ti $yr $hr $min $hr12 $ampm $utc_offset/;
751 ( $dw, $mo, $mo_num, $da, $ti, $yr, $utc_offset )
752 = nice_date( $files{"$path_file"} );
753 ( $hr, $min ) = split /:/, $ti;
754 ( $hr12, $ampm ) = $hr >= 12 ? ( $hr - 12, 'pm' ) : ( $hr, 'am' );
756 if ( $hr12 == 0 ) { $hr12 = 12 }
758 # Only stories from the right date
759 my ( $path_info_yr, $path_info_mo_num, $path_info_da )
761 next if $path_info_yr && $yr != $path_info_yr;
762 last if $path_info_yr && $yr < $path_info_yr;
763 next if $path_info_mo_num && $mo ne $num2month[$path_info_mo_num];
764 next if $path_info_da && $da != $path_info_da;
765 last if $path_info_da && $da < $path_info_da;
768 my $date = ( &$template( $path, 'date', $flavour ) );
771 foreach my $plugin (@plugins) {
772 if ( $plugins{$plugin} > 0 and $plugin->can('date') ) {
774 = $plugin->date( $currentdir, \$date,
775 $files{$path_file}, $dw, $mo, $mo_num, $da, $ti,
780 $date = &$interpolate($date);
782 if ( $date && $curdate ne $date ) {
787 use vars qw/ $title $body $raw /;
788 if ( -f "$path_file" && $fh->open("< $path_file") ) {
789 chomp( $title = <$fh> );
790 chomp( $body = join '', <$fh> );
792 $raw = "$title\n$body";
794 my $story = ( &$template( $path, 'story', $flavour ) );
797 foreach my $plugin (@plugins) {
798 if ( $plugins{$plugin} > 0 and $plugin->can('story') ) {
799 $entries = $plugin->story( $path, $fn, \$story, \$title,
804 if ( $encode_xml_entities &&
805 $content_type =~ m{\bxml\b} &&
806 $content_type !~ m{\bxhtml\b} ) {
807 # Escape special characters inside the <link> container
809 # The following line should be moved more towards to top for
810 # performance reasons -- Axel Beckert, 2008-07-22
811 my $url_escape_re = qr([^-/a-zA-Z0-9:._]);
813 $url =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
814 $path =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
815 $fn =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
817 # Escape <, >, and &, and to produce valid RSS
818 $title = blosxom_html_escape($title);
819 $body = blosxom_html_escape($body);
820 $url = blosxom_html_escape($url);
821 $path = blosxom_html_escape($path);
822 $fn = blosxom_html_escape($fn);
825 $story = &$interpolate($story);
834 my $foot = ( &$template( $currentdir, 'foot', $flavour ) );
837 foreach my $plugin (@plugins) {
838 if ( $plugins{$plugin} > 0 and $plugin->can('foot') ) {
839 $entries = $plugin->foot( $currentdir, \$foot );
843 $foot = &$interpolate($foot);
847 foreach my $plugin (@plugins) {
848 if ( $plugins{$plugin} > 0 and $plugin->can('last') ) {
849 $entries = $plugin->last();
855 # Finally, add the header, if any and running dynamically
856 $output = header($header) . $output
857 if ( $static_or_dynamic eq 'dynamic' and $header );
865 my $c_time = CORE::localtime($unixtime);
866 my ( $dw, $mo, $da, $hr, $min, $sec, $yr )
868 =~ /(\w{3}) +(\w{3}) +(\d{1,2}) +(\d{2}):(\d{2}):(\d{2}) +(\d{4})$/
871 $da = sprintf( "%02d", $da );
872 my $mo_num = $month2num{$mo};
875 = timegm( $sec, $min, $hr, $da, $mo_num - 1, $yr - 1900 ) - $unixtime;
876 my $utc_offset = sprintf( "%+03d", int( $offset / 3600 ) )
877 . sprintf( "%02d", ( $offset % 3600 ) / 60 );
879 return ( $dw, $mo, $mo_num, $da, $ti, $yr, $utc_offset );
882 # Default HTML and RSS template bits
884 html content_type text/html; charset=$blog_encoding
886 html head <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
889 html head <meta http-equiv="content-type" content="$content_type" >
890 html head <link rel="alternate" type="application/rss+xml" title="RSS" href="$url/index.rss" >
891 html head <title>$blog_title $path_info_da $path_info_mo $path_info_yr</title>
894 html head <div align="center">
895 html head <h1>$blog_title</h1>
896 html head <p>$path_info_da $path_info_mo $path_info_yr</p>
900 html story <h3><a name="$fn">$title</a></h3>
901 html story <div>$body</div>
902 html story <p>posted at: $ti | path: <a href="$url$path">$path</a> | <a href="$url/$yr/$mo_num/$da#$fn">permanent link to this entry</a></p>
905 html date <h2>$dw, $da $mo $yr</h2>
908 html foot <div align="center">
909 html foot <a href="http://blosxom.sourceforge.net/"><img src="http://blosxom.sourceforge.net/images/pb_blosxom.gif" alt="powered by blosxom" border="0" width="90" height="33" ></a>
914 rss content_type text/xml; charset=$blog_encoding
916 rss head <?xml version="1.0" encoding="$blog_encoding"?>
917 rss head <rss version="2.0">
919 rss head <title>$blog_title</title>
920 rss head <link>$url/$path_info</link>
921 rss head <description>$blog_description</description>
922 rss head <language>$blog_language</language>
923 rss head <docs>http://blogs.law.harvard.edu/tech/rss</docs>
924 rss head <generator>blosxom/$version</generator>
927 rss story <title>$title</title>
928 rss story <pubDate>$dw, $da $mo $yr $ti:00 $utc_offset</pubDate>
929 rss story <link>$url/$yr/$mo_num/$da#$fn</link>
930 rss story <category>$path</category>
931 rss story <guid isPermaLink="false">$url$path/$fn</guid>
932 rss story <description>$body</description>
940 error content_type text/html
942 error head <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
944 error head <head><title>Error: unknown Blosxom flavour "$flavour"</title></head>
946 error head <h1><font color="red">Error: unknown Blosxom flavour "$flavour"</font></h1>
947 error head <p>I'm afraid this is the first I've heard of a "$flavour" flavoured Blosxom. Try dropping the "/+$flavour" bit from the end of the URL.</p>
949 error story <h3>$title</h3>
950 error story <div>$body</div> <p><a href="$url/$yr/$mo_num/$da#fn.$default_flavour">#</a></p>
952 error date <h2>$dw, $da $mo $yr</h2>