4 # Author: Rael Dornfest (2002-2003), The Blosxom Development Team (2005-2008)
5 # Version: 2.1.2 ($Id: blosxom.cgi,v 1.88 2008/11/13 17:19:51 alfie 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 # Should I encode entities for xml content-types? (plugins can turn this off if they do it themselves)
130 $encode_xml_entities = 1;
132 # --------------------------------
138 =item B<BLOSXOM_CONFIG_FILE>
140 Points to the location of the configuration file. This will be
141 considered as first option, if it's set.
144 =item B<BLOSXOM_CONFIG_DIR>
146 The here named directory will be tried unless the above mentioned
147 environment variable is set and tested for a contained blosxom.conf
158 =item B</usr/lib/cgi-bin/blosxom>
160 The CGI script itself. Please note that the location might depend on
163 =item B</etc/blosxom/blosxom.conf>
165 The default configuration file location. This is rather taken as last
166 ressort if no other configuration location is set through environment
174 Rael Dornfest <rael@oreilly.com> was the original author of blosxom. The
175 development was picked up by a team of dedicated users of blosxom since
176 2005. See <I<http://blosxom.sourceforge.net/>> for more information.
182 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 !;
189 use CGI qw/:standard :netscape/;
193 # Load configuration from $ENV{BLOSXOM_CONFIG_DIR}/blosxom.conf, if it exists
195 if ( $ENV{BLOSXOM_CONFIG_FILE} && -r $ENV{BLOSXOM_CONFIG_FILE} ) {
196 $blosxom_config = $ENV{BLOSXOM_CONFIG_FILE};
197 ( $config_dir = $blosxom_config ) =~ s! / [^/]* $ !!x;
200 for my $blosxom_config_dir ( $ENV{BLOSXOM_CONFIG_DIR}, '/etc/blosxom',
203 if ( -r "$blosxom_config_dir/blosxom.conf" ) {
204 $config_dir = $blosxom_config_dir;
205 $blosxom_config = "$blosxom_config_dir/blosxom.conf";
211 # Load $blosxom_config
212 if ($blosxom_config) {
213 if ( -r $blosxom_config ) {
214 eval { require $blosxom_config }
215 or warn "Error reading blosxom config file '$blosxom_config'"
216 . ( $@ ? ": $@" : '' );
219 warn "Cannot find or read blosxom config file '$blosxom_config'";
223 my $fh = new FileHandle;
240 @num2month = sort { $month2num{$a} <=> $month2num{$b} } keys %month2num;
242 # Use the stated preferred URL or figure it out automatically. Set
243 # $url manually in the config section above if CGI.pm doesn't guess
244 # the base URL correctly, e.g. when called from a Server Side Includes
249 # Unescape %XX hex codes (from URI::Escape::uri_unescape)
250 $url =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
252 # Support being called from inside a SSI document
253 $url =~ s/^included:/http:/ if $ENV{SERVER_PROTOCOL} eq 'INCLUDED';
255 # Remove PATH_INFO if it is set but not removed by CGI.pm. This
256 # seems to happen when used with Apache's Alias directive or if
257 # called from inside a Server Side Include document. If that
258 # doesn't help either, set $url manually in the configuration.
259 $url =~ s/\Q$ENV{PATH_INFO}\E$// if defined $ENV{PATH_INFO};
263 # There is one case where this code does more than necessary, too:
264 # If the URL requested is e.g. http://example.org/blog/blog and
265 # the base URL is correctly determined as http://example.org/blog
266 # by CGI.pm, then this code will incorrectly normalize the base
267 # URL down to http://example.org, because the same string as
268 # PATH_INFO is part of the base URL, too. But this is such a
269 # seldom case and can be fixed by setting $url in the config file,
273 # The only modification done to a manually set base URL is to strip
274 # a trailing slash if present.
278 # Drop ending any / from dir settings
280 $plugin_dir =~ s!/$!!;
281 $static_dir =~ s!/$!!;
283 # Fix depth to take into account datadir's path
284 $depth += ( $datadir =~ tr[/][] ) - 1 if $depth;
286 if ( !$ENV{GATEWAY_INTERFACE}
287 and param('-password')
289 and param('-password') eq $static_password )
291 $static_or_dynamic = 'static';
294 $static_or_dynamic = 'dynamic';
295 param( -name => '-quiet', -value => 1 );
299 # Take a gander at HTTP's PATH_INFO for optional blog name, archive yr/mo/day
300 my @path_info = split m{/}, path_info() || param('path');
301 $path_info_full = join '/', @path_info; # Equivalent to $ENV{PATH_INFO}
304 # Flavour specified by ?flav={flav} or index.{flav}
306 if (! ($flavour = param('flav'))) {
307 if ( $path_info[$#path_info] =~ /(.+)\.(.+)$/ ) {
309 pop @path_info if $1 eq 'index';
312 $flavour ||= $default_flavour;
314 # Fix XSS in flavour name (CVE-2008-2236)
315 $flavour = blosxom_html_escape($flavour);
317 sub blosxom_html_escape {
326 my $escape_re = join '|' => keys %escape;
327 $string =~ s/($escape_re)/$escape{$1}/g;
331 # Global variable to be used in head/foot.{flavour} templates
333 # Add all @path_info elements to $path_info till we come to one that could be a year
334 while ( $path_info[0] && $path_info[0] !~ /^(19|20)\d{2}$/) {
335 $path_info .= '/' . shift @path_info;
338 # Pull date elements out of path
339 if ($path_info[0] && $path_info[0] =~ /^(19|20)\d{2}$/) {
340 $path_info_yr = shift @path_info;
342 ($path_info[0] =~ /^(0\d|1[012])$/ ||
343 exists $month2num{ ucfirst lc $path_info_mo })) {
344 $path_info_mo = shift @path_info;
345 # Map path_info_mo to numeric $path_info_mo_num
346 $path_info_mo_num = $path_info_mo =~ /^\d{2}$/
348 : $month2num{ ucfirst lc $path_info_mo };
349 if ($path_info[0] && $path_info[0] =~ /^[0123]\d$/) {
350 $path_info_da = shift @path_info;
355 # Add remaining path elements to $path_info
356 $path_info .= '/' . join('/', @path_info);
358 # Strip spurious slashes
359 $path_info =~ s!(^/*)|(/*$)!!g;
361 # Define standard template subroutine, plugin-overridable at Plugins: Template
363 my ( $path, $chunk, $flavour ) = @_;
366 return join '', <$fh>
367 if $fh->open("< $datadir/$path/$chunk.$flavour");
368 } while ( $path =~ s/(\/*[^\/]*)$// and $1 );
370 # Check for definedness, since flavour can be the empty string
371 if ( defined $template{$flavour}{$chunk} ) {
372 return $template{$flavour}{$chunk};
374 elsif ( defined $template{error}{$chunk} ) {
375 return $template{error}{$chunk};
382 # Bring in the templates
385 last if /^(__END__)$/;
386 my ( $ct, $comp, $txt ) = /^(\S+)\s(\S+)(?:\s(.*))?$/ or next;
388 $template{$ct}{$comp} .= $txt . "\n";
392 my $path_sep = $^O eq 'MSWin32' ? ';' : ':';
393 my @plugin_dirs = split /$path_sep/, $plugin_path;
394 unshift @plugin_dirs, $plugin_dir;
395 my @plugin_list = ();
396 my %plugin_hash = ();
398 # If $plugin_list is set, read plugins to use from that file
399 if ( $plugin_list ) {
400 if ( -r $plugin_list and $fh->open("< $plugin_list") ) {
401 @plugin_list = map { chomp $_; $_ } grep { /\S/ && !/^#/ } <$fh>;
405 warn "unable to read or open plugin_list '$plugin_list': $!";
410 # Otherwise walk @plugin_dirs to get list of plugins to use
411 if ( ! @plugin_list && @plugin_dirs ) {
412 for my $plugin_dir (@plugin_dirs) {
413 next unless -d $plugin_dir;
414 if ( opendir PLUGINS, $plugin_dir ) {
416 grep { /^[\w:]+$/ && !/~$/ && -f "$plugin_dir/$_" }
421 next if $plugin_hash{$plugin};
423 # Add to @plugin_list and %plugin_hash
424 $plugin_hash{$plugin} = "$plugin_dir/$plugin";
425 push @plugin_list, $plugin;
430 @plugin_list = sort @plugin_list;
433 # Load all plugins in @plugin_list
434 unshift @INC, @plugin_dirs;
435 foreach my $plugin (@plugin_list) {
436 my ( $plugin_name, $off ) = $plugin =~ /^\d*([\w:]+?)(_?)$/;
437 my $plugin_file = $plugin_list ? $plugin_name : $plugin;
438 my $on_off = $off eq '_' ? -1 : 1;
440 # Allow perl module plugins
441 # The -z test is a hack to allow a zero-length placeholder file in a
442 # $plugin_path directory to indicate an @INC module should be loaded
443 if ( $plugin =~ m/::/ && ( $plugin_list || -z $plugin_hash{$plugin} ) ) {
445 # For Blosxom::Plugin::Foo style plugins, we need to use a string require
446 eval "require $plugin_file";
449 { # we try first to load from $plugin_dir before attempting from $plugin_path
450 eval { require "$plugin_dir/$plugin_file" }
451 or eval { require $plugin_file };
455 warn "error finding or loading blosxom plugin '$plugin_name': $@";
458 if ( $plugin_name->start() and ( $plugins{$plugin_name} = $on_off ) ) {
459 push @plugins, $plugin_name;
463 shift @INC foreach @plugin_dirs;
466 # Allow for the first encountered plugin::template subroutine to override the
467 # default built-in template subroutine
468 foreach my $plugin (@plugins) {
469 if ( $plugins{$plugin} > 0 and $plugin->can('template') ) {
470 if ( my $tmp = $plugin->template() ) {
477 # Provide backward compatibility for Blosxom < 2.0rc1 plug-ins
479 return &$template(@_);
482 # Define default entries subroutine
484 my ( %files, %indexes, %others );
488 my $curr_depth = $File::Find::dir =~ tr[/][];
489 return if $depth and $curr_depth > $depth;
495 =~ m!^$datadir/(?:(.*)/)?(.+)\.$file_extension$!
497 # not an index, .file, and is readable
498 and $2 ne 'index' and $2 !~ /^\./ and ( -r $File::Find::name )
502 # read modification time
503 my $mtime = stat($File::Find::name)->mtime or return;
505 # to show or not to show future entries
506 return unless ( $show_future_entries or $mtime < time );
508 # add the file and its associated mtime to the list of files
509 $files{$File::Find::name} = $mtime;
511 # static rendering bits
513 = "$static_dir/$1/index." . $static_flavours[0];
516 or stat($static_file)->mtime < $mtime )
519 $d = join( '/', ( nice_date($mtime) )[ 5, 2, 3 ] );
521 $indexes{ ( $1 ? "$1/" : '' ) . "$2.$file_extension" } = 1
526 # not an entries match
527 elsif ( !-d $File::Find::name and -r $File::Find::name ) {
528 $others{$File::Find::name} = stat($File::Find::name)->mtime;
534 return ( \%files, \%indexes, \%others );
538 # Allow for the first encountered plugin::entries subroutine to override the
539 # default built-in entries subroutine
540 foreach my $plugin (@plugins) {
541 if ( $plugins{$plugin} > 0 and $plugin->can('entries') ) {
542 if ( my $tmp = $plugin->entries() ) {
549 my ( $files, $indexes, $others ) = &$entries();
550 %indexes = %$indexes;
553 if ( !$ENV{GATEWAY_INTERFACE}
554 and param('-password')
556 and param('-password') eq $static_password )
559 param('-quiet') or print "Blosxom is generating static index pages...\n";
561 # Home Page and Directory Indexes
563 foreach my $path ( sort keys %indexes ) {
565 foreach ( ( '', split /\//, $path ) ) {
569 mkdir "$static_dir/$p", 0755
570 unless ( -d "$static_dir/$p" or $p =~ /\.$file_extension$/ );
571 foreach $flavour (@static_flavours) {
573 = ( &$template( $p, 'content_type', $flavour ) );
574 $content_type =~ s!\n.*!!s;
575 my $fn = $p =~ m!^(.+)\.$file_extension$! ? $1 : "$p/index";
576 param('-quiet') or print "$fn.$flavour\n";
577 my $fh_w = new FileHandle "> $static_dir/$fn.$flavour"
578 or die "Couldn't open $static_dir/$p for writing: $!";
580 if ( $indexes{$path} == 1 ) {
586 $path_info =~ s!\.$file_extension$!\.$flavour!;
587 print $fh_w &generate( 'static', $path_info, '', $flavour,
594 $path_info_yr, $path_info_mo,
595 $path_info_da, $path_info
596 ) = split /\//, $p, 4;
597 unless ( defined $path_info ) { $path_info = "" }
598 print $fh_w &generate( 'static', '', $p, $flavour,
609 $content_type = ( &$template( $path_info, 'content_type', $flavour ) );
610 $content_type =~ s!\n.*!!s;
612 $content_type =~ s/(\$\w+(?:::\w+)*)/"defined $1 ? $1 : ''"/gee;
613 $header = { -type => $content_type };
615 print generate( 'dynamic', $path_info,
616 "$path_info_yr/$path_info_mo_num/$path_info_da",
617 $flavour, $content_type );
621 foreach my $plugin (@plugins) {
622 if ( $plugins{$plugin} > 0 and $plugin->can('end') ) {
623 $entries = $plugin->end();
629 my ( $static_or_dynamic, $currentdir, $date, $flavour, $content_type )
633 %others = ref $others ? %$others : ();
636 foreach my $plugin (@plugins) {
637 if ( $plugins{$plugin} > 0 and $plugin->can('filter') ) {
638 $entries = $plugin->filter( \%files, \%others );
645 # Allow plugins to decide if we can cut short story generation
647 foreach my $plugin (@plugins) {
648 if ( $plugins{$plugin} > 0 and $plugin->can('skip') ) {
649 if ( my $tmp = $plugin->skip() ) {
656 # Define default interpolation subroutine
659 my $template = shift;
660 # Interpolate scalars, namespaced scalars, and hash/hashref scalars
661 $template =~ s/(\$\w+(?:::\w+)*(?:(?:->)?{(['"]?)[-\w]+\2})?)/"defined $1 ? $1 : ''"/gee;
665 unless ( defined($skip) and $skip ) {
667 # Plugins: Interpolate
668 # Allow for the first encountered plugin::interpolate subroutine to
669 # override the default built-in interpolate subroutine
670 foreach my $plugin (@plugins) {
671 if ( $plugins{$plugin} > 0 and $plugin->can('interpolate') ) {
672 if ( my $tmp = $plugin->interpolate() ) {
680 my $head = ( &$template( $currentdir, 'head', $flavour ) );
683 foreach my $plugin (@plugins) {
684 if ( $plugins{$plugin} > 0 and $plugin->can('head') ) {
685 $entries = $plugin->head( $currentdir, \$head );
689 $head = &$interpolate($head);
695 my $ne = $num_entries;
697 if ( $currentdir =~ /(.*?)([^\/]+)\.(.+)$/ and $2 ne 'index' ) {
698 $currentdir = "$1$2.$file_extension";
699 %f = ( "$datadir/$currentdir" => $files{"$datadir/$currentdir"} )
700 if $files{"$datadir/$currentdir"};
703 $currentdir =~ s!/index\..+$!!;
706 # Define a default sort subroutine
708 my ($files_ref) = @_;
710 sort { $files_ref->{$b} <=> $files_ref->{$a} }
715 # Allow for the first encountered plugin::sort subroutine to override the
716 # default built-in sort subroutine
717 foreach my $plugin (@plugins) {
718 if ( $plugins{$plugin} > 0 and $plugin->can('sort') ) {
719 if ( my $tmp = $plugin->sort() ) {
726 foreach my $path_file ( &$sort( \%f, \%others ) ) {
727 last if $ne <= 0 && $date !~ /\d/;
728 use vars qw/ $path $fn /;
730 = $path_file =~ m!^$datadir/(?:(.*)/)?(.*)\.$file_extension!;
732 # Only stories in the right hierarchy
733 $path =~ /^$currentdir/
734 or $path_file eq "$datadir/$currentdir"
737 # Prepend a slash for use in templates only if a path exists
740 # Date fiddling for by-{year,month,day} archive views
742 qw/ $dw $mo $mo_num $da $ti $yr $hr $min $hr12 $ampm $utc_offset/;
743 ( $dw, $mo, $mo_num, $da, $ti, $yr, $utc_offset )
744 = nice_date( $files{"$path_file"} );
745 ( $hr, $min ) = split /:/, $ti;
746 ( $hr12, $ampm ) = $hr >= 12 ? ( $hr - 12, 'pm' ) : ( $hr, 'am' );
748 if ( $hr12 == 0 ) { $hr12 = 12 }
750 # Only stories from the right date
751 my ( $path_info_yr, $path_info_mo_num, $path_info_da )
753 next if $path_info_yr && $yr != $path_info_yr;
754 last if $path_info_yr && $yr < $path_info_yr;
755 next if $path_info_mo_num && $mo ne $num2month[$path_info_mo_num];
756 next if $path_info_da && $da != $path_info_da;
757 last if $path_info_da && $da < $path_info_da;
760 my $date = ( &$template( $path, 'date', $flavour ) );
763 foreach my $plugin (@plugins) {
764 if ( $plugins{$plugin} > 0 and $plugin->can('date') ) {
766 = $plugin->date( $currentdir, \$date,
767 $files{$path_file}, $dw, $mo, $mo_num, $da, $ti,
772 $date = &$interpolate($date);
774 if ( $date && $curdate ne $date ) {
779 use vars qw/ $title $body $raw /;
780 if ( -f "$path_file" && $fh->open("< $path_file") ) {
781 chomp( $title = <$fh> );
782 chomp( $body = join '', <$fh> );
784 $raw = "$title\n$body";
786 my $story = ( &$template( $path, 'story', $flavour ) );
789 foreach my $plugin (@plugins) {
790 if ( $plugins{$plugin} > 0 and $plugin->can('story') ) {
791 $entries = $plugin->story( $path, $fn, \$story, \$title,
796 if ( $encode_xml_entities &&
797 $content_type =~ m{\bxml\b} &&
798 $content_type !~ m{\bxhtml\b} ) {
799 # Escape special characters inside the <link> container
801 # The following line should be moved more towards to top for
802 # performance reasons -- Axel Beckert, 2008-07-22
803 my $url_escape_re = qr([^-/a-zA-Z0-9:._]);
805 $url =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
806 $path =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
807 $fn =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
809 # Escape <, >, and &, and to produce valid RSS
810 $title = blosxom_html_escape($title);
811 $body = blosxom_html_escape($body);
812 $url = blosxom_html_escape($url);
813 $path = blosxom_html_escape($path);
814 $fn = blosxom_html_escape($fn);
817 $story = &$interpolate($story);
826 my $foot = ( &$template( $currentdir, 'foot', $flavour ) );
829 foreach my $plugin (@plugins) {
830 if ( $plugins{$plugin} > 0 and $plugin->can('foot') ) {
831 $entries = $plugin->foot( $currentdir, \$foot );
835 $foot = &$interpolate($foot);
839 foreach my $plugin (@plugins) {
840 if ( $plugins{$plugin} > 0 and $plugin->can('last') ) {
841 $entries = $plugin->last();
847 # Finally, add the header, if any and running dynamically
848 $output = header($header) . $output
849 if ( $static_or_dynamic eq 'dynamic' and $header );
857 my $c_time = CORE::localtime($unixtime);
858 my ( $dw, $mo, $da, $hr, $min, $sec, $yr )
860 =~ /(\w{3}) +(\w{3}) +(\d{1,2}) +(\d{2}):(\d{2}):(\d{2}) +(\d{4})$/
863 $da = sprintf( "%02d", $da );
864 my $mo_num = $month2num{$mo};
867 = timegm( $sec, $min, $hr, $da, $mo_num - 1, $yr - 1900 ) - $unixtime;
868 my $utc_offset = sprintf( "%+03d", int( $offset / 3600 ) )
869 . sprintf( "%02d", ( $offset % 3600 ) / 60 );
871 return ( $dw, $mo, $mo_num, $da, $ti, $yr, $utc_offset );
874 # Default HTML and RSS template bits
876 html content_type text/html; charset=$blog_encoding
878 html head <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
881 html head <meta http-equiv="content-type" content="$content_type" >
882 html head <link rel="alternate" type="application/rss+xml" title="RSS" href="$url/index.rss" >
883 html head <title>$blog_title $path_info_da $path_info_mo $path_info_yr</title>
886 html head <div align="center">
887 html head <h1>$blog_title</h1>
888 html head <p>$path_info_da $path_info_mo $path_info_yr</p>
892 html story <h3><a name="$fn">$title</a></h3>
893 html story <div>$body</div>
894 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>
897 html date <h2>$dw, $da $mo $yr</h2>
900 html foot <div align="center">
901 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>
906 rss content_type text/xml; charset=$blog_encoding
908 rss head <?xml version="1.0" encoding="$blog_encoding"?>
909 rss head <rss version="2.0">
911 rss head <title>$blog_title</title>
912 rss head <link>$url/$path_info</link>
913 rss head <description>$blog_description</description>
914 rss head <language>$blog_language</language>
915 rss head <docs>http://blogs.law.harvard.edu/tech/rss</docs>
916 rss head <generator>blosxom/$version</generator>
919 rss story <title>$title</title>
920 rss story <pubDate>$dw, $da $mo $yr $ti:00 $utc_offset</pubDate>
921 rss story <link>$url/$yr/$mo_num/$da#$fn</link>
922 rss story <category>$path</category>
923 rss story <guid isPermaLink="false">$url$path/$fn</guid>
924 rss story <description>$body</description>
932 error content_type text/html
934 error head <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
936 error head <head><title>Error: unknown Blosxom flavour "$flavour"</title></head>
938 error head <h1><font color="red">Error: unknown Blosxom flavour "$flavour"</font></h1>
939 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>
941 error story <h3>$title</h3>
942 error story <div>$body</div> <p><a href="$url/$yr/$mo_num/$da#fn.$default_flavour">#</a></p>
944 error date <h2>$dw, $da $mo $yr</h2>