4 # Author: Rael Dornfest (2002-2003), The Blosxom Development Team (2005-2008)
5 # Version: 2.1.2 ($Id: blosxom.cgi,v 1.90 2009/03/08 00:50:55 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 automatic)?
81 # Should I stick only to the datadir for items or travel down the
82 # directory hierarchy looking for items? If so, to what depth?
83 # 0 = infinite depth (aka grab everything), 1 = datadir only, n = n levels down
86 # How many entries should I show on the home page?
89 # What file extension signifies a blosxom entry?
90 $file_extension = "txt";
92 # What is the default flavour?
93 $default_flavour = "html";
95 # Should I show entries from the future (i.e. dated after now)?
96 $show_future_entries = 0;
98 # --- Plugins (Optional) -----
100 # File listing plugins blosxom should load
101 # (if empty blosxom will load all plugins in $plugin_dir and $plugin_path directories)
104 # Where are my plugins kept?
107 # Where should my plugins keep their state information?
108 $plugin_state_dir = "$plugin_dir/state";
110 # Additional plugins location
111 # List of directories, separated by ';' on windows, ':' everywhere else
114 # --- Static Rendering -----
116 # Where are this blog's static files to be created?
117 $static_dir = "/Library/WebServer/Documents/blog";
119 # What's my administrative password (you must set this for static rendering)?
120 $static_password = "";
122 # What flavours should I generate statically?
123 @static_flavours = qw/html rss/;
125 # Should I statically generate individual entries?
129 # --- Advanced Encoding Options -----
131 # Should I encode entities for xml content-types? (plugins can turn this off if they do it themselves)
132 $encode_xml_entities = 1;
134 # --------------------------------
140 =item B<BLOSXOM_CONFIG_FILE>
142 Points to the location of the configuration file. This will be
143 considered as first option, if it's set.
146 =item B<BLOSXOM_CONFIG_DIR>
148 The here named directory will be tried unless the above mentioned
149 environment variable is set and tested for a contained blosxom.conf
160 =item B</usr/lib/cgi-bin/blosxom>
162 The CGI script itself. Please note that the location might depend on
165 =item B</etc/blosxom/blosxom.conf>
167 The default configuration file location. This is rather taken as last
168 ressort if no other configuration location is set through environment
176 Rael Dornfest <rael@oreilly.com> was the original author of blosxom. The
177 development was picked up by a team of dedicated users of blosxom since
178 2005. See <I<http://blosxom.sourceforge.net/>> for more information.
184 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 !;
191 use CGI qw/:standard :netscape/;
193 $version = "2.1.2+dev";
195 # Load configuration from $ENV{BLOSXOM_CONFIG_DIR}/blosxom.conf, if it exists
197 if ( $ENV{BLOSXOM_CONFIG_FILE} && -r $ENV{BLOSXOM_CONFIG_FILE} ) {
198 $blosxom_config = $ENV{BLOSXOM_CONFIG_FILE};
199 ( $config_dir = $blosxom_config ) =~ s! / [^/]* $ !!x;
202 for my $blosxom_config_dir ( $ENV{BLOSXOM_CONFIG_DIR}, '/etc/blosxom',
205 if ( -r "$blosxom_config_dir/blosxom.conf" ) {
206 $config_dir = $blosxom_config_dir;
207 $blosxom_config = "$blosxom_config_dir/blosxom.conf";
213 # Load $blosxom_config
214 if ($blosxom_config) {
215 if ( -r $blosxom_config ) {
216 eval { require $blosxom_config }
217 or warn "Error reading blosxom config file '$blosxom_config'"
218 . ( $@ ? ": $@" : '' );
221 warn "Cannot find or read blosxom config file '$blosxom_config'";
225 my $fh = new FileHandle;
242 @num2month = sort { $month2num{$a} <=> $month2num{$b} } keys %month2num;
244 # Use the stated preferred URL or figure it out automatically. Set
245 # $url manually in the config section above if CGI.pm doesn't guess
246 # the base URL correctly, e.g. when called from a Server Side Includes
251 # Unescape %XX hex codes (from URI::Escape::uri_unescape)
252 $url =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
254 # Support being called from inside a SSI document
255 $url =~ s/^included:/http:/ if $ENV{SERVER_PROTOCOL} eq 'INCLUDED';
257 # Remove PATH_INFO if it is set but not removed by CGI.pm. This
258 # seems to happen when used with Apache's Alias directive or if
259 # called from inside a Server Side Include document. If that
260 # doesn't help either, set $url manually in the configuration.
261 $url =~ s/\Q$ENV{PATH_INFO}\E$// if defined $ENV{PATH_INFO};
265 # There is one case where this code does more than necessary, too:
266 # If the URL requested is e.g. http://example.org/blog/blog and
267 # the base URL is correctly determined as http://example.org/blog
268 # by CGI.pm, then this code will incorrectly normalize the base
269 # URL down to http://example.org, because the same string as
270 # PATH_INFO is part of the base URL, too. But this is such a
271 # seldom case and can be fixed by setting $url in the config file,
275 # The only modification done to a manually set base URL is to strip
276 # a trailing slash if present.
280 # Drop ending any / from dir settings
282 $plugin_dir =~ s!/$!!;
283 $static_dir =~ s!/$!!;
285 # Fix depth to take into account datadir's path
286 $depth += ( $datadir =~ tr[/][] ) - 1 if $depth;
288 if ( !$ENV{GATEWAY_INTERFACE}
289 and param('-password')
291 and param('-password') eq $static_password )
293 $static_or_dynamic = 'static';
296 $static_or_dynamic = 'dynamic';
297 param( -name => '-quiet', -value => 1 );
301 # Take a gander at HTTP's PATH_INFO for optional blog name, archive yr/mo/day
302 my @path_info = split m{/}, path_info() || param('path');
303 $path_info_full = join '/', @path_info; # Equivalent to $ENV{PATH_INFO}
306 # Flavour specified by ?flav={flav} or index.{flav}
308 if (! ($flavour = param('flav'))) {
309 if ( $path_info[$#path_info] =~ /(.+)\.(.+)$/ ) {
311 pop @path_info if $1 eq 'index';
314 $flavour ||= $default_flavour;
316 # Fix XSS in flavour name (CVE-2008-2236)
317 $flavour = blosxom_html_escape($flavour);
319 sub blosxom_html_escape {
328 my $escape_re = join '|' => keys %escape;
329 $string =~ s/($escape_re)/$escape{$1}/g;
333 # Global variable to be used in head/foot.{flavour} templates
335 # Add all @path_info elements to $path_info till we come to one that could be a year
336 while ( $path_info[0] && $path_info[0] !~ /^(19|20)\d{2}$/) {
337 $path_info .= '/' . shift @path_info;
340 # Pull date elements out of path
341 if ($path_info[0] && $path_info[0] =~ /^(19|20)\d{2}$/) {
342 $path_info_yr = shift @path_info;
344 ($path_info[0] =~ /^(0\d|1[012])$/ ||
345 exists $month2num{ ucfirst lc $path_info_mo })) {
346 $path_info_mo = shift @path_info;
347 # Map path_info_mo to numeric $path_info_mo_num
348 $path_info_mo_num = $path_info_mo =~ /^\d{2}$/
350 : $month2num{ ucfirst lc $path_info_mo };
351 if ($path_info[0] && $path_info[0] =~ /^[0123]\d$/) {
352 $path_info_da = shift @path_info;
357 # Add remaining path elements to $path_info
358 $path_info .= '/' . join('/', @path_info);
360 # Strip spurious slashes
361 $path_info =~ s!(^/*)|(/*$)!!g;
363 # Define standard template subroutine, plugin-overridable at Plugins: Template
365 my ( $path, $chunk, $flavour ) = @_;
368 return join '', <$fh>
369 if $fh->open("< $datadir/$path/$chunk.$flavour");
370 } while ( $path =~ s/(\/*[^\/]*)$// and $1 );
372 # Check for definedness, since flavour can be the empty string
373 if ( defined $template{$flavour}{$chunk} ) {
374 return $template{$flavour}{$chunk};
376 elsif ( defined $template{error}{$chunk} ) {
377 return $template{error}{$chunk};
384 # Bring in the templates
387 last if /^(__END__)$/;
388 my ( $ct, $comp, $txt ) = /^(\S+)\s(\S+)(?:\s(.*))?$/ or next;
390 $template{$ct}{$comp} .= $txt . "\n";
394 my $path_sep = $^O eq 'MSWin32' ? ';' : ':';
395 my @plugin_dirs = split /$path_sep/, $plugin_path;
396 unshift @plugin_dirs, $plugin_dir;
397 my @plugin_list = ();
398 my %plugin_hash = ();
400 # If $plugin_list is set, read plugins to use from that file
401 if ( $plugin_list ) {
402 if ( -r $plugin_list and $fh->open("< $plugin_list") ) {
403 @plugin_list = map { chomp $_; $_ } grep { /\S/ && !/^#/ } <$fh>;
407 warn "unable to read or open plugin_list '$plugin_list': $!";
412 # Otherwise walk @plugin_dirs to get list of plugins to use
413 if ( ! @plugin_list && @plugin_dirs ) {
414 for my $plugin_dir (@plugin_dirs) {
415 next unless -d $plugin_dir;
416 if ( opendir PLUGINS, $plugin_dir ) {
418 grep { /^[\w:]+$/ && !/~$/ && -f "$plugin_dir/$_" }
423 next if $plugin_hash{$plugin};
425 # Add to @plugin_list and %plugin_hash
426 $plugin_hash{$plugin} = "$plugin_dir/$plugin";
427 push @plugin_list, $plugin;
432 @plugin_list = sort @plugin_list;
435 # Load all plugins in @plugin_list
436 unshift @INC, @plugin_dirs;
437 foreach my $plugin (@plugin_list) {
438 my ( $plugin_name, $off ) = $plugin =~ /^\d*([\w:]+?)(_?)$/;
439 my $plugin_file = $plugin_list ? $plugin_name : $plugin;
440 my $on_off = $off eq '_' ? -1 : 1;
442 # Allow perl module plugins
443 # The -z test is a hack to allow a zero-length placeholder file in a
444 # $plugin_path directory to indicate an @INC module should be loaded
445 if ( $plugin =~ m/::/ && ( $plugin_list || -z $plugin_hash{$plugin} ) ) {
447 # For Blosxom::Plugin::Foo style plugins, we need to use a string require
448 eval "require $plugin_file";
451 { # we try first to load from $plugin_dir before attempting from $plugin_path
452 eval { require "$plugin_dir/$plugin_file" }
453 or eval { require $plugin_file };
457 warn "error finding or loading blosxom plugin '$plugin_name': $@";
460 if ( $plugin_name->start() and ( $plugins{$plugin_name} = $on_off ) ) {
461 push @plugins, $plugin_name;
465 shift @INC foreach @plugin_dirs;
468 # Allow for the first encountered plugin::template subroutine to override the
469 # default built-in template subroutine
470 foreach my $plugin (@plugins) {
471 if ( $plugins{$plugin} > 0 and $plugin->can('template') ) {
472 if ( my $tmp = $plugin->template() ) {
479 # Provide backward compatibility for Blosxom < 2.0rc1 plug-ins
481 return &$template(@_);
484 # Define default entries subroutine
486 my ( %files, %indexes, %others );
490 my $curr_depth = $File::Find::dir =~ tr[/][];
491 return if $depth and $curr_depth > $depth;
497 =~ m!^$datadir/(?:(.*)/)?(.+)\.$file_extension$!
499 # not an index, .file, and is readable
500 and $2 ne 'index' and $2 !~ /^\./ and ( -r $File::Find::name )
504 # read modification time
505 my $mtime = stat($File::Find::name)->mtime or return;
507 # to show or not to show future entries
508 return unless ( $show_future_entries or $mtime < time );
510 # add the file and its associated mtime to the list of files
511 $files{$File::Find::name} = $mtime;
513 # static rendering bits
515 = "$static_dir/$1/index." . $static_flavours[0];
518 or stat($static_file)->mtime < $mtime )
521 $d = join( '/', ( nice_date($mtime) )[ 5, 2, 3 ] );
523 $indexes{ ( $1 ? "$1/" : '' ) . "$2.$file_extension" } = 1
528 # not an entries match
529 elsif ( !-d $File::Find::name and -r $File::Find::name ) {
530 $others{$File::Find::name} = stat($File::Find::name)->mtime;
536 return ( \%files, \%indexes, \%others );
540 # Allow for the first encountered plugin::entries subroutine to override the
541 # default built-in entries subroutine
542 foreach my $plugin (@plugins) {
543 if ( $plugins{$plugin} > 0 and $plugin->can('entries') ) {
544 if ( my $tmp = $plugin->entries() ) {
551 my ( $files, $indexes, $others ) = &$entries();
552 %indexes = %$indexes;
555 if ( !$ENV{GATEWAY_INTERFACE}
556 and param('-password')
558 and param('-password') eq $static_password )
561 param('-quiet') or print "Blosxom is generating static index pages...\n";
563 # Home Page and Directory Indexes
565 foreach my $path ( sort keys %indexes ) {
567 foreach ( ( '', split /\//, $path ) ) {
571 mkdir "$static_dir/$p", 0755
572 unless ( -d "$static_dir/$p" or $p =~ /\.$file_extension$/ );
573 foreach $flavour (@static_flavours) {
575 = ( &$template( $p, 'content_type', $flavour ) );
576 $content_type =~ s!\n.*!!s;
577 my $fn = $p =~ m!^(.+)\.$file_extension$! ? $1 : "$p/index";
578 param('-quiet') or print "$fn.$flavour\n";
579 my $fh_w = new FileHandle "> $static_dir/$fn.$flavour"
580 or die "Couldn't open $static_dir/$p for writing: $!";
582 if ( $indexes{$path} == 1 ) {
588 $path_info =~ s!\.$file_extension$!\.$flavour!;
589 print $fh_w &generate( 'static', $path_info, '', $flavour,
596 $path_info_yr, $path_info_mo,
597 $path_info_da, $path_info
598 ) = split /\//, $p, 4;
599 unless ( defined $path_info ) { $path_info = "" }
600 print $fh_w &generate( 'static', '', $p, $flavour,
611 $content_type = ( &$template( $path_info, 'content_type', $flavour ) );
612 $content_type =~ s!\n.*!!s;
614 $content_type =~ s/(\$\w+(?:::\w+)*)/"defined $1 ? $1 : ''"/gee;
615 $header = { -type => $content_type };
617 print generate( 'dynamic', $path_info,
618 "$path_info_yr/$path_info_mo_num/$path_info_da",
619 $flavour, $content_type );
623 foreach my $plugin (@plugins) {
624 if ( $plugins{$plugin} > 0 and $plugin->can('end') ) {
625 $entries = $plugin->end();
631 my ( $static_or_dynamic, $currentdir, $date, $flavour, $content_type )
635 %others = ref $others ? %$others : ();
638 foreach my $plugin (@plugins) {
639 if ( $plugins{$plugin} > 0 and $plugin->can('filter') ) {
640 $entries = $plugin->filter( \%files, \%others );
647 # Allow plugins to decide if we can cut short story generation
649 foreach my $plugin (@plugins) {
650 if ( $plugins{$plugin} > 0 and $plugin->can('skip') ) {
651 if ( my $tmp = $plugin->skip() ) {
658 # Define default interpolation subroutine
661 my $template = shift;
662 # Interpolate scalars, namespaced scalars, and hash/hashref scalars
663 $template =~ s/(\$\w+(?:::\w+)*(?:(?:->)?{(['"]?)[-\w]+\2})?)/"defined $1 ? $1 : ''"/gee;
667 unless ( defined($skip) and $skip ) {
669 # Plugins: Interpolate
670 # Allow for the first encountered plugin::interpolate subroutine to
671 # override the default built-in interpolate subroutine
672 foreach my $plugin (@plugins) {
673 if ( $plugins{$plugin} > 0 and $plugin->can('interpolate') ) {
674 if ( my $tmp = $plugin->interpolate() ) {
682 my $head = ( &$template( $currentdir, 'head', $flavour ) );
685 foreach my $plugin (@plugins) {
686 if ( $plugins{$plugin} > 0 and $plugin->can('head') ) {
687 $entries = $plugin->head( $currentdir, \$head );
691 $head = &$interpolate($head);
697 my $ne = $num_entries;
699 if ( $currentdir =~ /(.*?)([^\/]+)\.(.+)$/ and $2 ne 'index' ) {
700 $currentdir = "$1$2.$file_extension";
701 %f = ( "$datadir/$currentdir" => $files{"$datadir/$currentdir"} )
702 if $files{"$datadir/$currentdir"};
705 $currentdir =~ s!/index\..+$!!;
708 # Define a default sort subroutine
710 my ($files_ref) = @_;
712 sort { $files_ref->{$b} <=> $files_ref->{$a} }
717 # Allow for the first encountered plugin::sort subroutine to override the
718 # default built-in sort subroutine
719 foreach my $plugin (@plugins) {
720 if ( $plugins{$plugin} > 0 and $plugin->can('sort') ) {
721 if ( my $tmp = $plugin->sort() ) {
728 foreach my $path_file ( &$sort( \%f, \%others ) ) {
729 last if $ne <= 0 && $date !~ /\d/;
730 use vars qw/ $path $fn /;
732 = $path_file =~ m!^$datadir/(?:(.*)/)?(.*)\.$file_extension!;
734 # Only stories in the right hierarchy
735 $path =~ /^$currentdir/
736 or $path_file eq "$datadir/$currentdir"
739 # Prepend a slash for use in templates only if a path exists
742 # Date fiddling for by-{year,month,day} archive views
744 qw/ $dw $mo $mo_num $da $ti $yr $hr $min $hr12 $ampm $utc_offset/;
745 ( $dw, $mo, $mo_num, $da, $ti, $yr, $utc_offset )
746 = nice_date( $files{"$path_file"} );
747 ( $hr, $min ) = split /:/, $ti;
748 ( $hr12, $ampm ) = $hr >= 12 ? ( $hr - 12, 'pm' ) : ( $hr, 'am' );
750 if ( $hr12 == 0 ) { $hr12 = 12 }
752 # Only stories from the right date
753 my ( $path_info_yr, $path_info_mo_num, $path_info_da )
755 next if $path_info_yr && $yr != $path_info_yr;
756 last if $path_info_yr && $yr < $path_info_yr;
757 next if $path_info_mo_num && $mo ne $num2month[$path_info_mo_num];
758 next if $path_info_da && $da != $path_info_da;
759 last if $path_info_da && $da < $path_info_da;
762 my $date = ( &$template( $path, 'date', $flavour ) );
765 foreach my $plugin (@plugins) {
766 if ( $plugins{$plugin} > 0 and $plugin->can('date') ) {
768 = $plugin->date( $currentdir, \$date,
769 $files{$path_file}, $dw, $mo, $mo_num, $da, $ti,
774 $date = &$interpolate($date);
776 if ( $date && $curdate ne $date ) {
781 use vars qw/ $title $body $raw /;
782 if ( -f "$path_file" && $fh->open("< $path_file") ) {
783 chomp( $title = <$fh> );
784 chomp( $body = join '', <$fh> );
786 $raw = "$title\n$body";
788 my $story = ( &$template( $path, 'story', $flavour ) );
791 foreach my $plugin (@plugins) {
792 if ( $plugins{$plugin} > 0 and $plugin->can('story') ) {
793 $entries = $plugin->story( $path, $fn, \$story, \$title,
798 if ( $encode_xml_entities &&
799 $content_type =~ m{\bxml\b} &&
800 $content_type !~ m{\bxhtml\b} ) {
801 # Escape special characters inside the <link> container
803 # The following line should be moved more towards to top for
804 # performance reasons -- Axel Beckert, 2008-07-22
805 my $url_escape_re = qr([^-/a-zA-Z0-9:._]);
807 $url =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
808 $path =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
809 $fn =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
811 # Escape <, >, and &, and to produce valid RSS
812 $title = blosxom_html_escape($title);
813 $body = blosxom_html_escape($body);
814 $url = blosxom_html_escape($url);
815 $path = blosxom_html_escape($path);
816 $fn = blosxom_html_escape($fn);
819 $story = &$interpolate($story);
828 my $foot = ( &$template( $currentdir, 'foot', $flavour ) );
831 foreach my $plugin (@plugins) {
832 if ( $plugins{$plugin} > 0 and $plugin->can('foot') ) {
833 $entries = $plugin->foot( $currentdir, \$foot );
837 $foot = &$interpolate($foot);
841 foreach my $plugin (@plugins) {
842 if ( $plugins{$plugin} > 0 and $plugin->can('last') ) {
843 $entries = $plugin->last();
849 # Finally, add the header, if any and running dynamically
850 $output = header($header) . $output
851 if ( $static_or_dynamic eq 'dynamic' and $header );
859 my $c_time = CORE::localtime($unixtime);
860 my ( $dw, $mo, $da, $hr, $min, $sec, $yr )
862 =~ /(\w{3}) +(\w{3}) +(\d{1,2}) +(\d{2}):(\d{2}):(\d{2}) +(\d{4})$/
865 $da = sprintf( "%02d", $da );
866 my $mo_num = $month2num{$mo};
869 = timegm( $sec, $min, $hr, $da, $mo_num - 1, $yr - 1900 ) - $unixtime;
870 my $utc_offset = sprintf( "%+03d", int( $offset / 3600 ) )
871 . sprintf( "%02d", ( $offset % 3600 ) / 60 );
873 return ( $dw, $mo, $mo_num, $da, $ti, $yr, $utc_offset );
876 # Default HTML and RSS template bits
878 html content_type text/html; charset=$blog_encoding
880 html head <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
883 html head <meta http-equiv="content-type" content="$content_type" >
884 html head <link rel="alternate" type="application/rss+xml" title="RSS" href="$url/index.rss" >
885 html head <title>$blog_title $path_info_da $path_info_mo $path_info_yr</title>
888 html head <div align="center">
889 html head <h1>$blog_title</h1>
890 html head <p>$path_info_da $path_info_mo $path_info_yr</p>
894 html story <h3><a name="$fn">$title</a></h3>
895 html story <div>$body</div>
896 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>
899 html date <h2>$dw, $da $mo $yr</h2>
902 html foot <div align="center">
903 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>
908 rss content_type text/xml; charset=$blog_encoding
910 rss head <?xml version="1.0" encoding="$blog_encoding"?>
911 rss head <rss version="2.0">
913 rss head <title>$blog_title</title>
914 rss head <link>$url/$path_info</link>
915 rss head <description>$blog_description</description>
916 rss head <language>$blog_language</language>
917 rss head <docs>http://blogs.law.harvard.edu/tech/rss</docs>
918 rss head <generator>blosxom/$version</generator>
921 rss story <title>$title</title>
922 rss story <pubDate>$dw, $da $mo $yr $ti:00 $utc_offset</pubDate>
923 rss story <link>$url/$yr/$mo_num/$da#$fn</link>
924 rss story <category>$path</category>
925 rss story <guid isPermaLink="false">$url$path/$fn</guid>
926 rss story <description>$body</description>
934 error content_type text/html
936 error head <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
938 error head <head><title>Error: unknown Blosxom flavour "$flavour"</title></head>
940 error head <h1><font color="red">Error: unknown Blosxom flavour "$flavour"</font></h1>
941 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>
943 error story <h3>$title</h3>
944 error story <div>$body</div> <p><a href="$url/$yr/$mo_num/$da#fn.$default_flavour">#</a></p>
946 error date <h2>$dw, $da $mo $yr</h2>