4 # Author: Rael Dornfest (2002-2003), The Blosxom Development Team (2005-2009)
5 # Version: 2.1.2 ($Id: blosxom.cgi,v 1.95 2009/03/08 01:28:06 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 # Should I encode 8 bit special characters, e.g. umlauts in URLs, e.g.
141 # convert an ISO-Latin-1 \"o to %F6? (off by default for now; plugins
142 # can change this, too)
143 $encode_8bit_chars = 0;
145 # --------------------------------
151 =item B<BLOSXOM_CONFIG_FILE>
153 Points to the location of the configuration file. This will be
154 considered as first option, if it's set.
157 =item B<BLOSXOM_CONFIG_DIR>
159 The here named directory will be tried unless the above mentioned
160 environment variable is set and tested for a contained blosxom.conf
171 =item B</usr/lib/cgi-bin/blosxom>
173 The CGI script itself. Please note that the location might depend on
176 =item B</etc/blosxom/blosxom.conf>
178 The default configuration file location. This is rather taken as last
179 ressort if no other configuration location is set through environment
187 Rael Dornfest <rael@oreilly.com> was the original author of blosxom. The
188 development was picked up by a team of dedicated users of blosxom since
189 2005. See <I<http://blosxom.sourceforge.net/>> for more information.
248 use CGI qw/:standard :netscape/;
250 $version = "2.1.2+dev";
252 # Load configuration from $ENV{BLOSXOM_CONFIG_DIR}/blosxom.conf, if it exists
254 if ( $ENV{BLOSXOM_CONFIG_FILE} && -r $ENV{BLOSXOM_CONFIG_FILE} ) {
255 $blosxom_config = $ENV{BLOSXOM_CONFIG_FILE};
256 ( $config_dir = $blosxom_config ) =~ s! / [^/]* $ !!x;
259 for my $blosxom_config_dir ( $ENV{BLOSXOM_CONFIG_DIR}, '/etc/blosxom',
262 if ( -r "$blosxom_config_dir/blosxom.conf" ) {
263 $config_dir = $blosxom_config_dir;
264 $blosxom_config = "$blosxom_config_dir/blosxom.conf";
270 # Load $blosxom_config
271 if ($blosxom_config) {
272 if ( -r $blosxom_config ) {
273 eval { require $blosxom_config }
274 or warn "Error reading blosxom config file '$blosxom_config'"
275 . ( $@ ? ": $@" : '' );
278 warn "Cannot find or read blosxom config file '$blosxom_config'";
282 my $fh = new FileHandle;
299 @num2month = sort { $month2num{$a} <=> $month2num{$b} } keys %month2num;
301 # Use the stated preferred URL or figure it out automatically. Set
302 # $url manually in the config section above if CGI.pm doesn't guess
303 # the base URL correctly, e.g. when called from a Server Side Includes
308 # Unescape %XX hex codes (from URI::Escape::uri_unescape)
309 $url =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
311 # Support being called from inside a SSI document
312 $url =~ s/^included:/http:/ if $ENV{SERVER_PROTOCOL} eq 'INCLUDED';
314 # Remove PATH_INFO if it is set but not removed by CGI.pm. This
315 # seems to happen when used with Apache's Alias directive or if
316 # called from inside a Server Side Include document. If that
317 # doesn't help either, set $url manually in the configuration.
318 $url =~ s/\Q$ENV{PATH_INFO}\E$// if defined $ENV{PATH_INFO};
322 # There is one case where this code does more than necessary, too:
323 # If the URL requested is e.g. http://example.org/blog/blog and
324 # the base URL is correctly determined as http://example.org/blog
325 # by CGI.pm, then this code will incorrectly normalize the base
326 # URL down to http://example.org, because the same string as
327 # PATH_INFO is part of the base URL, too. But this is such a
328 # seldom case and can be fixed by setting $url in the config file,
332 # The only modification done to a manually set base URL is to strip
333 # a trailing slash if present.
337 # Drop ending any / from dir settings
339 $plugin_dir =~ s!/$!!;
340 $static_dir =~ s!/$!!;
342 # Fix depth to take into account datadir's path
343 $depth += ( $datadir =~ tr[/][] ) - 1 if $depth;
345 if ( !$ENV{GATEWAY_INTERFACE}
346 and param('-password')
348 and param('-password') eq $static_password )
350 $static_or_dynamic = 'static';
353 $static_or_dynamic = 'dynamic';
354 param( -name => '-quiet', -value => 1 );
358 # Take a gander at HTTP's PATH_INFO for optional blog name, archive yr/mo/day
359 my @path_info = split m{/}, path_info() || param('path');
360 $path_info_full = join '/', @path_info; # Equivalent to $ENV{PATH_INFO}
363 # Flavour specified by ?flav={flav} or index.{flav}
365 if (! ($flavour = param('flav'))) {
366 if ( $path_info[$#path_info] =~ /(.+)\.(.+)$/ ) {
368 pop @path_info if $1 eq 'index';
371 $flavour ||= $default_flavour;
373 # Fix XSS in flavour name (CVE-2008-2236)
374 $flavour = blosxom_html_escape($flavour);
376 sub blosxom_html_escape {
385 my $escape_re = join '|' => keys %escape;
386 $string =~ s/($escape_re)/$escape{$1}/g;
390 # Global variable to be used in head/foot.{flavour} templates
392 # Add all @path_info elements to $path_info till we come to one that could be a year
393 while ( $path_info[0] && $path_info[0] !~ /^(19|20)\d{2}$/) {
394 $path_info .= '/' . shift @path_info;
397 # Pull date elements out of path
398 if ($path_info[0] && $path_info[0] =~ /^(19|20)\d{2}$/) {
399 $path_info_yr = shift @path_info;
401 ($path_info[0] =~ /^(0\d|1[012])$/ ||
402 exists $month2num{ ucfirst lc $path_info_mo })) {
403 $path_info_mo = shift @path_info;
404 # Map path_info_mo to numeric $path_info_mo_num
405 $path_info_mo_num = $path_info_mo =~ /^\d{2}$/
407 : $month2num{ ucfirst lc $path_info_mo };
408 if ($path_info[0] && $path_info[0] =~ /^[0123]\d$/) {
409 $path_info_da = shift @path_info;
414 # Add remaining path elements to $path_info
415 $path_info .= '/' . join('/', @path_info);
417 # Strip spurious slashes
418 $path_info =~ s!(^/*)|(/*$)!!g;
420 # Define standard template subroutine, plugin-overridable at Plugins: Template
422 my ( $path, $chunk, $flavour ) = @_;
425 return join '', <$fh>
426 if $fh->open("< $datadir/$path/$chunk.$flavour");
427 } while ( $path =~ s/(\/*[^\/]*)$// and $1 );
429 # Check for definedness, since flavour can be the empty string
430 if ( defined $template{$flavour}{$chunk} ) {
431 return $template{$flavour}{$chunk};
433 elsif ( defined $template{error}{$chunk} ) {
434 return $template{error}{$chunk};
441 # Bring in the templates
444 last if /^(__END__)$/;
445 my ( $ct, $comp, $txt ) = /^(\S+)\s(\S+)(?:\s(.*))?$/ or next;
447 $template{$ct}{$comp} .= $txt . "\n";
451 my $path_sep = $^O eq 'MSWin32' ? ';' : ':';
452 my @plugin_dirs = split /$path_sep/, $plugin_path;
453 unshift @plugin_dirs, $plugin_dir;
454 my @plugin_list = ();
455 my %plugin_hash = ();
457 # If $plugin_list is set, read plugins to use from that file
458 if ( $plugin_list ) {
459 if ( -r $plugin_list and $fh->open("< $plugin_list") ) {
460 @plugin_list = map { chomp $_; $_ } grep { /\S/ && !/^#/ } <$fh>;
464 warn "unable to read or open plugin_list '$plugin_list': $!";
469 # Otherwise walk @plugin_dirs to get list of plugins to use
470 if ( ! @plugin_list && @plugin_dirs ) {
471 for my $plugin_dir (@plugin_dirs) {
472 next unless -d $plugin_dir;
473 if ( opendir PLUGINS, $plugin_dir ) {
475 grep { /^[\w:]+$/ && !/~$/ && -f "$plugin_dir/$_" }
480 next if $plugin_hash{$plugin};
482 # Add to @plugin_list and %plugin_hash
483 $plugin_hash{$plugin} = "$plugin_dir/$plugin";
484 push @plugin_list, $plugin;
489 @plugin_list = sort @plugin_list;
492 # Load all plugins in @plugin_list
493 unshift @INC, @plugin_dirs;
494 foreach my $plugin (@plugin_list) {
495 my ( $plugin_name, $off ) = $plugin =~ /^\d*([\w:]+?)(_?)$/;
496 my $plugin_file = $plugin_list ? $plugin_name : $plugin;
497 my $on_off = $off eq '_' ? -1 : 1;
499 # Allow perl module plugins
500 # The -z test is a hack to allow a zero-length placeholder file in a
501 # $plugin_path directory to indicate an @INC module should be loaded
502 if ( $plugin =~ m/::/ && ( $plugin_list || -z $plugin_hash{$plugin} ) ) {
504 # For Blosxom::Plugin::Foo style plugins, we need to use a string require
505 eval "require $plugin_file";
508 { # we try first to load from $plugin_dir before attempting from $plugin_path
509 eval { require "$plugin_dir/$plugin_file" }
510 or eval { require $plugin_file };
514 warn "error finding or loading blosxom plugin '$plugin_name': $@";
517 if ( $plugin_name->start() and ( $plugins{$plugin_name} = $on_off ) ) {
518 push @plugins, $plugin_name;
522 shift @INC foreach @plugin_dirs;
525 # Allow for the first encountered plugin::template subroutine to override the
526 # default built-in template subroutine
527 foreach my $plugin (@plugins) {
528 if ( $plugins{$plugin} > 0 and $plugin->can('template') ) {
529 if ( my $tmp = $plugin->template() ) {
536 # Provide backward compatibility for Blosxom < 2.0rc1 plug-ins
538 return &$template(@_);
541 # Define default entries subroutine
543 my ( %files, %indexes, %others );
547 my $curr_depth = $File::Find::dir =~ tr[/][];
548 return if $depth and $curr_depth > $depth;
554 =~ m!^$datadir/(?:(.*)/)?(.+)\.$file_extension$!
556 # not an index, .file, and is readable
557 and $2 ne 'index' and $2 !~ /^\./ and ( -r $File::Find::name )
561 # read modification time
562 my $mtime = stat($File::Find::name)->mtime or return;
564 # to show or not to show future entries
565 return unless ( $show_future_entries or $mtime < time );
567 # add the file and its associated mtime to the list of files
568 $files{$File::Find::name} = $mtime;
570 # static rendering bits
572 = "$static_dir/$1/index." . $static_flavours[0];
575 or stat($static_file)->mtime < $mtime )
578 $d = join( '/', ( nice_date($mtime) )[ 5, 2, 3 ] );
580 $indexes{ ( $1 ? "$1/" : '' ) . "$2.$file_extension" } = 1
585 # not an entries match
586 elsif ( !-d $File::Find::name and -r $File::Find::name ) {
587 $others{$File::Find::name} = stat($File::Find::name)->mtime;
593 return ( \%files, \%indexes, \%others );
597 # Allow for the first encountered plugin::entries subroutine to override the
598 # default built-in entries subroutine
599 foreach my $plugin (@plugins) {
600 if ( $plugins{$plugin} > 0 and $plugin->can('entries') ) {
601 if ( my $tmp = $plugin->entries() ) {
608 my ( $files, $indexes, $others ) = &$entries();
609 %indexes = %$indexes;
612 if ( !$ENV{GATEWAY_INTERFACE}
613 and param('-password')
615 and param('-password') eq $static_password )
618 param('-quiet') or print "Blosxom is generating static index pages...\n";
620 # Home Page and Directory Indexes
622 foreach my $path ( sort keys %indexes ) {
624 foreach ( ( '', split /\//, $path ) ) {
628 mkdir "$static_dir/$p", 0755
629 unless ( -d "$static_dir/$p" or $p =~ /\.$file_extension$/ );
630 foreach $flavour (@static_flavours) {
632 = ( &$template( $p, 'content_type', $flavour ) );
633 $content_type =~ s!\n.*!!s;
634 my $fn = $p =~ m!^(.+)\.$file_extension$! ? $1 : "$p/index";
635 param('-quiet') or print "$fn.$flavour\n";
636 my $fh_w = new FileHandle "> $static_dir/$fn.$flavour"
637 or die "Couldn't open $static_dir/$p for writing: $!";
639 if ( $indexes{$path} == 1 ) {
645 $path_info =~ s!\.$file_extension$!\.$flavour!;
646 print $fh_w &generate( 'static', $path_info, '', $flavour,
653 $path_info_yr, $path_info_mo,
654 $path_info_da, $path_info
655 ) = split /\//, $p, 4;
656 unless ( defined $path_info ) { $path_info = "" }
657 print $fh_w &generate( 'static', '', $p, $flavour,
668 $content_type = ( &$template( $path_info, 'content_type', $flavour ) );
669 $content_type =~ s!\n.*!!s;
671 $content_type =~ s/(\$\w+(?:::\w+)*)/"defined $1 ? $1 : ''"/gee;
672 $header = { -type => $content_type };
674 print generate( 'dynamic', $path_info,
675 "$path_info_yr/$path_info_mo_num/$path_info_da",
676 $flavour, $content_type );
680 foreach my $plugin (@plugins) {
681 if ( $plugins{$plugin} > 0 and $plugin->can('end') ) {
682 $entries = $plugin->end();
688 my ( $static_or_dynamic, $currentdir, $date, $flavour, $content_type )
692 %others = ref $others ? %$others : ();
695 foreach my $plugin (@plugins) {
696 if ( $plugins{$plugin} > 0 and $plugin->can('filter') ) {
697 $entries = $plugin->filter( \%files, \%others );
704 # Allow plugins to decide if we can cut short story generation
706 foreach my $plugin (@plugins) {
707 if ( $plugins{$plugin} > 0 and $plugin->can('skip') ) {
708 if ( my $tmp = $plugin->skip() ) {
715 # Define default interpolation subroutine
718 my $template = shift;
719 # Interpolate scalars, namespaced scalars, and hash/hashref scalars
720 $template =~ s/(\$\w+(?:::\w+)*(?:(?:->)?{([\'\"]?)[-\w]+\2})?)/"defined $1 ? $1 : ''"/gee;
724 unless ( defined($skip) and $skip ) {
726 # Plugins: Interpolate
727 # Allow for the first encountered plugin::interpolate subroutine to
728 # override the default built-in interpolate subroutine
729 foreach my $plugin (@plugins) {
730 if ( $plugins{$plugin} > 0 and $plugin->can('interpolate') ) {
731 if ( my $tmp = $plugin->interpolate() ) {
739 my $head = ( &$template( $currentdir, 'head', $flavour ) );
742 foreach my $plugin (@plugins) {
743 if ( $plugins{$plugin} > 0 and $plugin->can('head') ) {
744 $entries = $plugin->head( $currentdir, \$head );
748 $head = &$interpolate($head);
754 my $ne = $num_entries;
756 if ( $currentdir =~ /(.*?)([^\/]+)\.(.+)$/ and $2 ne 'index' ) {
757 $currentdir = "$1$2.$file_extension";
758 %f = ( "$datadir/$currentdir" => $files{"$datadir/$currentdir"} )
759 if $files{"$datadir/$currentdir"};
762 $currentdir =~ s!/index\..+$!!;
765 # Define a default sort subroutine
767 my ($files_ref) = @_;
769 sort { $files_ref->{$b} <=> $files_ref->{$a} }
774 # Allow for the first encountered plugin::sort subroutine to override the
775 # default built-in sort subroutine
776 foreach my $plugin (@plugins) {
777 if ( $plugins{$plugin} > 0 and $plugin->can('sort') ) {
778 if ( my $tmp = $plugin->sort() ) {
785 foreach my $path_file ( &$sort( \%f, \%others ) ) {
786 last if $ne <= 0 && $date !~ /\d/;
787 use vars qw/ $path $fn /;
789 = $path_file =~ m!^$datadir/(?:(.*)/)?(.*)\.$file_extension!;
791 # Only stories in the right hierarchy
792 $path =~ /^$currentdir/
793 or $path_file eq "$datadir/$currentdir"
796 # Prepend a slash for use in templates only if a path exists
799 # Date fiddling for by-{year,month,day} archive views
801 qw/ $dw $mo $mo_num $da $ti $yr $hr $min $hr12 $ampm $utc_offset/;
802 ( $dw, $mo, $mo_num, $da, $ti, $yr, $utc_offset )
803 = nice_date( $files{"$path_file"} );
804 ( $hr, $min ) = split /:/, $ti;
805 ( $hr12, $ampm ) = $hr >= 12 ? ( $hr - 12, 'pm' ) : ( $hr, 'am' );
807 if ( $hr12 == 0 ) { $hr12 = 12 }
809 # Only stories from the right date
810 my ( $path_info_yr, $path_info_mo_num, $path_info_da )
812 next if $path_info_yr && $yr != $path_info_yr;
813 last if $path_info_yr && $yr < $path_info_yr;
814 next if $path_info_mo_num && $mo ne $num2month[$path_info_mo_num];
815 next if $path_info_da && $da != $path_info_da;
816 last if $path_info_da && $da < $path_info_da;
819 my $date = ( &$template( $path, 'date', $flavour ) );
822 foreach my $plugin (@plugins) {
823 if ( $plugins{$plugin} > 0 and $plugin->can('date') ) {
825 = $plugin->date( $currentdir, \$date,
826 $files{$path_file}, $dw, $mo, $mo_num, $da, $ti,
831 $date = &$interpolate($date);
833 if ( $date && $curdate ne $date ) {
838 use vars qw/ $title $body $raw /;
839 if ( -f "$path_file" && $fh->open("< $path_file") ) {
840 chomp( $title = <$fh> );
841 chomp( $body = join '', <$fh> );
843 $raw = "$title\n$body";
845 my $story = ( &$template( $path, 'story', $flavour ) );
848 foreach my $plugin (@plugins) {
849 if ( $plugins{$plugin} > 0 and $plugin->can('story') ) {
850 $entries = $plugin->story( $path, $fn, \$story, \$title,
855 if ( $encode_xml_entities &&
856 $content_type =~ m{\bxml\b} &&
857 $content_type !~ m{\bxhtml\b} ) {
858 # Escape special characters inside the <link> container
860 # The following line should be moved more towards to top for
861 # performance reasons -- Axel Beckert, 2008-07-22
862 my $url_escape_re = qr([^-/a-zA-Z0-9:._]);
864 $url =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
865 $path =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
866 $fn =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
868 # Escape <, >, and &, and to produce valid RSS
869 $title = blosxom_html_escape($title);
870 $body = blosxom_html_escape($body);
871 $url = blosxom_html_escape($url);
872 $path = blosxom_html_escape($path);
873 $fn = blosxom_html_escape($fn);
876 if ($encode_8bit_chars) {
877 $url =~ s([^-a-zA-Z0-9_./:])(sprintf('%%%02X', ord($&)))ge;
878 $path =~ s([^-a-zA-Z0-9_./:])(sprintf('%%%02X', ord($&)))ge;
879 $fn =~ s([^-a-zA-Z0-9_./:])(sprintf('%%%02X', ord($&)))ge;
882 $story = &$interpolate($story);
891 my $foot = ( &$template( $currentdir, 'foot', $flavour ) );
894 foreach my $plugin (@plugins) {
895 if ( $plugins{$plugin} > 0 and $plugin->can('foot') ) {
896 $entries = $plugin->foot( $currentdir, \$foot );
900 $foot = &$interpolate($foot);
904 foreach my $plugin (@plugins) {
905 if ( $plugins{$plugin} > 0 and $plugin->can('last') ) {
906 $entries = $plugin->last();
912 # Finally, add the header, if any and running dynamically
913 $output = header($header) . $output
914 if ( $static_or_dynamic eq 'dynamic' and $header );
922 my $c_time = CORE::localtime($unixtime);
923 my ( $dw, $mo, $da, $hr, $min, $sec, $yr )
925 =~ /(\w{3}) +(\w{3}) +(\d{1,2}) +(\d{2}):(\d{2}):(\d{2}) +(\d{4})$/
928 $da = sprintf( "%02d", $da );
929 my $mo_num = $month2num{$mo};
932 = timegm( $sec, $min, $hr, $da, $mo_num - 1, $yr - 1900 ) - $unixtime;
933 my $utc_offset = sprintf( "%+03d", int( $offset / 3600 ) )
934 . sprintf( "%02d", ( $offset % 3600 ) / 60 );
936 return ( $dw, $mo, $mo_num, $da, $ti, $yr, $utc_offset );
939 # Default HTML and RSS template bits
941 html content_type text/html; charset=$blog_encoding
943 html head <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
946 html head <meta http-equiv="content-type" content="$content_type" >
947 html head <link rel="alternate" type="application/rss+xml" title="RSS" href="$url/index.rss" >
948 html head <title>$blog_title $path_info_da $path_info_mo $path_info_yr</title>
951 html head <div align="center">
952 html head <h1>$blog_title</h1>
953 html head <p>$path_info_da $path_info_mo $path_info_yr</p>
957 html story <h3><a name="$fn">$title</a></h3>
958 html story <div>$body</div>
959 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>
962 html date <h2>$dw, $da $mo $yr</h2>
965 html foot <div align="center">
966 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>
971 rss content_type text/xml; charset=$blog_encoding
973 rss head <?xml version="1.0" encoding="$blog_encoding"?>
974 rss head <rss version="2.0">
976 rss head <title>$blog_title</title>
977 rss head <link>$url/$path_info</link>
978 rss head <description>$blog_description</description>
979 rss head <language>$blog_language</language>
980 rss head <docs>http://blogs.law.harvard.edu/tech/rss</docs>
981 rss head <generator>blosxom/$version</generator>
984 rss story <title>$title</title>
985 rss story <pubDate>$dw, $da $mo $yr $ti:00 $utc_offset</pubDate>
986 rss story <link>$url/$yr/$mo_num/$da#$fn</link>
987 rss story <category>$path</category>
988 rss story <guid isPermaLink="false">$url$path/$fn</guid>
989 rss story <description>$body</description>
997 error content_type text/html
999 error head <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
1001 error head <head><title>Error: unknown Blosxom flavour "$flavour"</title></head>
1003 error head <h1><font color="red">Error: unknown Blosxom flavour "$flavour"</font></h1>
1004 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>
1006 error story <h3>$title</h3>
1007 error story <div>$body</div> <p><a href="$url/$yr/$mo_num/$da#fn.$default_flavour">#</a></p>
1009 error date <h2>$dw, $da $mo $yr</h2>