Annotated blosxom.cgi

    1  #!/usr/bin/perl
    2  
    3  # Blosxom                                                               # [1]
                                                                               # [2]
                                                                               # [3]
    4  # Author: Rael Dornfest <rael@oreilly.com>
    5  # Version: 2.0
    6  # Home/Docs/Licensing: http://www.raelity.org/apps/blosxom/
    7  
    8  package blosxom;                                                        # [4]
    9  
   10  # --- Configurable variables -----                                      # [5]
   11  
   12  # What's this blog's title?
   13  $blog_title = "My Weblog";                                              # [6]
   14  
   15  # What's this blog's description (for outgoing RSS feed)?
   16  $blog_description = "Yet another Blosxom weblog.";
   17  
   18  # What's this blog's primary language (for outgoing RSS feed)?
   19  $blog_language = "en";
   20  
   21  # Where are this blog's entries kept?
   22  $datadir = "/Library/WebServer/Documents/blosxom";
   23  
   24  # What's my preferred base URL for this blog (leave blank for automatic)?
   25  $url = "";
   26  
   27  # Should I stick only to the datadir for items or travel down the
   28  # directory hierarchy looking for items?  If so, to what depth?
   29  # 0 = infinite depth (aka grab everything), 1 = datadir only, n = n levels down
   30  $depth = 0;
   31  
   32  # How many entries should I show on the home page?
   33  $num_entries = 40;
   34  
   35  # What file extension signifies a blosxom entry?
   36  $file_extension = "txt";
   37  
   38  # What is the default flavour?
   39  $default_flavour = "html";
   40  
   41  # Should I show entries from the future (i.e. dated after now)?
   42  $show_future_entries = 0;
   43  
   44  # --- Plugins (Optional) -----
   45  
   46  # Where are my plugins kept?
   47  $plugin_dir = "";
   48  
   49  # Where should my modules keep their state information?
   50  $plugin_state_dir = "$plugin_dir/state";
   51  
   52  # --- Static Rendering -----
   53  
   54  # Where are this blog's static files to be created?
   55  $static_dir = "/Library/WebServer/Documents/blog";
   56  
   57  # What's my administrative password (you must set this for static rendering)?
   58  $static_password = "";
   59  
   60  # What flavours should I generate statically?
   61  @static_flavours = qw/html rss/;                                        # [7]
   62  
   63  # Should I statically generate individual entries?
   64  # 0 = no, 1 = yes
   65  $static_entries = 0;
   66  
   67  # --------------------------------
   68  
   69  use vars qw! $version $blog_title $blog_description $blog_language $datadir
       $url %template $template $depth $num_entries $file_extension $default_flavour
       $static_or_dynamic $plugin_dir $plugin_state_dir @plugins %plugins $static_dir
       $static_password @static_flavours $static_entries $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 !;                         # [8]
   70  
   71  use strict;                                                             # [9]
   72  use FileHandle;                                                         # [10]
                                                                               # [11]
   73  use File::Find;                                                         # [12]
   74  use File::stat;                                                         # [13]
   75  use Time::localtime;                                                    # [14]
   76  use CGI qw/:standard :netscape/;                                        # [15]
   77  
   78  $version = "2.0";                                                       # [16]
   79  
   80  my $fh = new FileHandle;                                                # [17]
                                                                               # [18]
   81  
   82  %month2num = (nil=>'00', Jan=>'01', Feb=>'02', Mar=>'03', Apr=>'04', May=>'05',
       Jun=>'06', Jul=>'07', Aug=>'08', Sep=>'09', Oct=>'10', Nov=>'11', Dec=>'12');
                                                                               # [19]
   83  @num2month = sort { $month2num{$a} <=> $month2num{$b} } keys %month2num;
                                                                               # [20]
   84  
   85  # Use the stated preferred URL or figure it out automatically
   86  $url ||= url();                                                         # [21]
                                                                               # [22]
   87  $url =~ s/^included:/http:/; # Fix for Server Side Includes (SSI)       # [23]
                                                                               # [24]
   88  $url =~ s!/$!!;                                                         # [25]
   89  
   90  # Drop ending any / from dir settings
   91  $datadir =~ s!/$!!; $plugin_dir =~ s!/$!!; $static_dir =~ s!/$!!;
   92    
   93  # Fix depth to take into account datadir's path
   94  $depth and $depth += ($datadir =~ tr[/][]) - 1;                         # [26]
                                                                               # [27]
   95  
   96  # Global variable to be used in head/foot.{flavour} templates
   97  $path_info = '';
   98  
   99  $static_or_dynamic = (!$ENV{GATEWAY_INTERFACE} and param('-password') and
       $static_password and param('-password') eq $static_password) ? 'static' :
       'dynamic';                                                              # [28]
                                                                               # [29]
  100  $static_or_dynamic eq 'dynamic' and param(-name=>'-quiet', -value=>1);  # [30]
  101  
  102  # Path Info Magic
  103  # Take a gander at HTTP's PATH_INFO for optional blog name, archive yr/mo/day
  104  my @path_info = split m{/}, path_info() || param('path');               # [31]
                                                                               # [32]
  105  shift @path_info;                                                       # [33]
  106  
  107  while ($path_info[0] and $path_info[0] =~ /^[a-zA-Z].*$/ and $path_info[0] !~
       /(.*)\.(.*)/) { $path_info .= '/' . shift @path_info; }                 # [34]
                                                                               # [35]
  108  
  109  # Flavour specified by ?flav={flav} or index.{flav}
  110  $flavour = '';
  111  
  112  if ( $path_info[$#path_info] =~ /(.+)\.(.+)$/ ) {                       # [36]
                                                                               # [37]
  113    $flavour = $2;                                                        # [38]
  114    $1 ne 'index' and $path_info .= "/$1.$2";                             # [39]
  115    pop @path_info;                                                       # [40]
  116  } else {
  117    $flavour = param('flav') || $default_flavour;                         # [41]
  118  }
  119  
  120  # Strip spurious slashes
  121  $path_info =~ s!(^/*)|(/*$)!!g;                                         # [42]
  122  
  123  # Date fiddling
  124  ($path_info_yr,$path_info_mo,$path_info_da) = @path_info;               # [43]
                                                                               # [44]
  125  $path_info_mo_num = $path_info_mo ? ( $path_info_mo =~ /\d{2}/ ? $path_info_mo
       : ($month2num{ucfirst(lc $path_info_mo)} || undef) ) : undef;           # [45]
                                                                               # [46]
  126  
  127  # Define standard template subroutine, plugin-overridable at Plugins: Template
  128  $template =                                                             # [47]
                                                                               # [48]
  129    sub {
  130      my ($path, $chunk, $flavour) = @_;                                  # [49]
                                                                               # [50]
  131  
  132      do {                                                                # [51]
  133        return join '', <$fh> if $fh->open("< $datadir/$path/$chunk.$flavour");
                                                                               # [52]
  134      } while ($path =~ s/(\/*[^\/]*)$// and $1);                         # [53]
  135  
  136      return join '', ($template{$flavour}{$chunk} || $template{error}{$chunk} ||
       '');                                                                    # [54]
  137    };
  138  # Bring in the templates
  139  %template = ();                                                         # [55]
  140  while (<DATA>) {                                                        # [56]
                                                                               # [57]
  141    last if /^(__END__)?$/;                                               # [58]
  142    my($ct, $comp, $txt) = /^(\S+)\s(\S+)\s(.*)$/;                        # [59]
  143    $txt =~ s/\\n/\n/mg;                                                  # [60]
  144    $template{$ct}{$comp} = $txt;                                         # [61]
  145  }
  146  
  147  # Plugins: Start
  148  if ( $plugin_dir and opendir PLUGINS, $plugin_dir ) {                   # [62]
  149    foreach my $plugin ( grep { /^\w+$/ && -f "$plugin_dir/$_"  } sort
       readdir(PLUGINS) ) {                                                    # [63]
  150      my($plugin_name, $off) = $plugin =~ /^\d*(\w+?)(_?)$/;              # [64]
  151      my $on_off = $off eq '_' ? -1 : 1;                                  # [65]
  152      require "$plugin_dir/$plugin";                                      # [66]
  153      $plugin_name->start() and ( $plugins{$plugin_name} = $on_off ) and push
       @plugins, $plugin_name;                                                 # [67]
  154    }
  155    closedir PLUGINS;                                                     # [68]
  156  }
  157  
  158  # Plugins: Template
  159  # Allow for the first encountered plugin::template subroutine to override the
  160  # default built-in template subroutine
  161  my $tmp; foreach my $plugin ( @plugins ) { $plugins{$plugin} > 0 and
       $plugin->can('template') and defined($tmp = $plugin->template()) and $template
       = $tmp and last; }                                                      # [69]
                                                                               # [70]
                                                                               # [71]
  162  
  163  # Provide backward compatibility for Blosxom < 2.0rc1 plug-ins
  164  sub load_template {                                                     # [72]
  165    return &$template(@_);
  166  }
  167  
  168  # Define default find subroutine
  169  $entries =                                                              # [73]
  170    sub {
  171      my(%files, %indexes, %others);                                      # [74]
  172      find(                                                               # [75]
  173        sub {
  174          my $d; 
  175          my $curr_depth = $File::Find::dir =~ tr[/][];                   # [76]
  176          return if $depth and $curr_depth > $depth;                      # [77]
  177       
  178          if (                                                            # [78]
  179            # a match
  180            $File::Find::name =~ m!^$datadir/(?:(.*)/)?(.+)\.$file_extension$!
                                                                               # [79]
  181            # not an index, .file, and is readable
  182            and $2 ne 'index' and $2 !~ /^\./ and (-r $File::Find::name)  # [80]
  183          ) {
  184  
  185              # to show or not to show future entries                     # [81]
  186              (                                                           # [82]
  187                $show_future_entries
  188                or stat($File::Find::name)->mtime < time 
  189              )
  190  
  191                # add the file and its associated mtime to the list of files
  192                and $files{$File::Find::name} = stat($File::Find::name)->mtime
                                                                               # [83]
  193  
  194                  # static rendering bits
  195                  and (                                                   # [84]
  196                    param('-all')                                         # [85]
  197                    or !-f "$static_dir/$1/index." . $static_flavours[0]  # [86]
  198                    or stat("$static_dir/$1/index." . $static_flavours[0])->mtime
       < stat($File::Find::name)->mtime                                        # [87]
  199                  )
  200                    and $indexes{$1} = 1                                  # [88]
  201                      and $d = join('/',
       (nice_date($files{$File::Find::name}))[5,2,3])                          # [89]
  202    
  203                        and $indexes{$d} = $d                             # [90]
  204                          and $static_entries and $indexes{ ($1 ? "$1/" : '') .
       "$2.$file_extension" } = 1                                              # [91]
  205  
  206              } 
  207              else {
  208                !-d $File::Find::name and -r $File::Find::name and
       $others{$File::Find::name} = stat($File::Find::name)->mtime             # [92]
  209              }
  210        }, $datadir                                                       # [93]
  211      );
  212  
  213      return (\%files, \%indexes, \%others);                              # [94]
  214    };
  215  
  216  # Plugins: Entries
  217  # Allow for the first encountered plugin::entries subroutine to override the
  218  # default built-in entries subroutine
  219  my $tmp; foreach my $plugin ( @plugins ) { $plugins{$plugin} > 0 and
       $plugin->can('entries') and defined($tmp = $plugin->entries()) and $entries =
       $tmp and last; }                                                        # [95]
  220  
  221  my ($files, $indexes, $others) = &$entries();                           # [96]
  222  %files = %$files; %indexes = %$indexes; %others = ref $others ? %$others : ();
                                                                               # [97]
  223  
  224  # Plugins: Filter
  225  foreach my $plugin ( @plugins ) { $plugins{$plugin} > 0 and
       $plugin->can('filter') and $entries = $plugin->filter(\%files, \%others) }
                                                                               # [98]
  226  
  227  # Static
  228  if (!$ENV{GATEWAY_INTERFACE} and param('-password') and $static_password and
       param('-password') eq $static_password) {                               # [99]
  229  
  230    param('-quiet') or print "Blosxom is generating static index pages...\n";
                                                                               # [100]
  231  
  232    # Home Page and Directory Indexes
  233    my %done;                                                             # [101]
  234    foreach my $path ( sort keys %indexes) {                              # [102]
  235      my $p = '';                                                         # [103]
  236      foreach ( ('', split /\//, $path) ) {                               # [104]
  237        $p .= "/$_";                                                      # [105]
  238        $p =~ s!^/!!;
  239        $path_info = $p;                                                  # [106]
  240        $done{$p}++ and next;                                             # [107]
                                                                               # [108]
  241        (-d "$static_dir/$p" or $p =~ /\.$file_extension$/) or mkdir
       "$static_dir/$p", 0755;                                                 # [109]
  242        foreach $flavour ( @static_flavours ) {                           # [110]
  243          my $content_type = (&$template($p,'content_type',$flavour));    # [111]
  244          $content_type =~ s!\n.*!!s;
  245          my $fn = $p =~ m!^(.+)\.$file_extension$! ? $1 : "$p/index";    # [112]
  246          param('-quiet') or print "$fn.$flavour\n";
  247          my $fh_w = new FileHandle "> $static_dir/$fn.$flavour" or die "Couldn't
       open $static_dir/$p for writing: $!";                                   # [113]
  248          $output = '';                                                   # [114]
  249          print $fh_w                                                     # [115]
  250            $indexes{$path} == 1
  251              ? &generate('static', $p, '', $flavour, $content_type)      # [116]
  252              : &generate('static', '', $p, $flavour, $content_type);
  253          $fh_w->close;                                                   # [117]
  254        }
  255      }
  256    }
  257  }
  258  
  259  # Dynamic
  260  else {                                                                  # [118]
  261    my $content_type = (&$template($path_info,'content_type',$flavour));  # [119]
  262    $content_type =~ s!\n.*!!s;
  263  
  264    $header = {-type=>$content_type};                                     # [120]
                                                                               # [121]
  265  
  266    print generate('dynamic', $path_info,
       "$path_info_yr/$path_info_mo_num/$path_info_da", $flavour, $content_type);
                                                                               # [122]
  267  }
  268  
  269  # Plugins: End
  270  foreach my $plugin ( @plugins ) { $plugins{$plugin} > 0 and $plugin->can('end')
       and $entries = $plugin->end() }                                         # [123]
  271  
  272  # Generate                                                              # [124]
  273  sub generate {                                                          # [125]
  274    my($static_or_dynamic, $currentdir, $date, $flavour, $content_type) = @_;
                                                                               # [126]
  275  
  276    my %f = %files;                                                       # [127]
  277  
  278    # Plugins: Skip
  279    # Allow plugins to decide if we can cut short story generation
  280    my $skip; foreach my $plugin ( @plugins ) { $plugins{$plugin} > 0 and
       $plugin->can('skip') and defined($tmp = $plugin->skip()) and $skip = $tmp and
       last; }                                                                 # [128]
                                                                               # [129]
  281    
  282    # Define default interpolation subroutine
  283    $interpolate =                                                        # [130]
  284      sub {
  285        package blosxom;                                                  # [131]
  286        my $template = shift;
  287        $template =~                                                      # [132]
                                                                               # [133]
  288          s/(\$\w+(?:::)?\w*)/"defined $1 ? $1 : ''"/gee;
  289        return $template;
  290      };  
  291  
  292    unless (defined($skip) and $skip) {                                   # [134]
  293  
  294      # Plugins: Interpolate
  295      # Allow for the first encountered plugin::interpolate subroutine to 
  296      # override the default built-in interpolate subroutine
  297      my $tmp; foreach my $plugin ( @plugins ) { $plugins{$plugin} > 0 and
       $plugin->can('interpolate') and defined($tmp = $plugin->interpolate()) and
       $interpolate = $tmp and last; }                                         # [135]
  298          
  299      # Head
  300      my $head = (&$template($currentdir,'head',$flavour));               # [136]
  301    
  302      # Plugins: Head
  303      foreach my $plugin ( @plugins ) { $plugins{$plugin} > 0 and
       $plugin->can('head') and $entries = $plugin->head($currentdir, \$head) }
                                                                               # [137]
                                                                               # [138]
  304    
  305      $head = &$interpolate($head);                                       # [139]
  306    
  307      $output .= $head;
  308      
  309      # Stories
  310      my $curdate = '';
  311      my $ne = $num_entries;                                              # [140]
  312  
  313      if ( $currentdir =~ /(.*?)([^\/]+)\.(.+)$/ and $2 ne 'index' ) {    # [141]
                                                                               # [142]
  314        $currentdir = "$1$2.$file_extension";                             # [143]
  315        $files{"$datadir/$1$2.$file_extension"} and %f = (
       "$datadir/$1$2.$file_extension" => $files{"$datadir/$1$2.$file_extension"} );
                                                                               # [144]
  316      } 
  317      else { 
  318        $currentdir =~ s!/index\..+$!!;                                   # [145]
  319      }
  320  
  321      # Define a default sort subroutine
  322      my $sort = sub {                                                    # [146]
  323        my($files_ref) = @_;
  324        return sort { $files_ref->{$b} <=> $files_ref->{$a} } keys %$files_ref;
  325      };
  326    
  327      # Plugins: Sort
  328      # Allow for the first encountered plugin::sort subroutine to override the
  329      # default built-in sort subroutine
  330      my $tmp; foreach my $plugin ( @plugins ) { $plugins{$plugin} > 0 and
       $plugin->can('sort') and defined($tmp = $plugin->sort()) and $sort = $tmp and
       last; }                                                                 # [147]
  331    
  332      foreach my $path_file ( &$sort(\%f, \%others) ) {                   # [148]
  333        last if $ne <= 0 && $date !~ /\d/;                                # [149]
                                                                               # [150]
  334        use vars qw/ $path $fn /;                                         # [151]
  335        ($path,$fn) = $path_file =~ m!^$datadir/(?:(.*)/)?(.*)\.$file_extension!;
                                                                               # [152]
  336    
  337        # Only stories in the right hierarchy
  338        $path =~ /^$currentdir/ or $path_file eq "$datadir/$currentdir" or next;
                                                                               # [153]
  339    
  340        # Prepend a slash for use in templates only if a path exists
  341        $path &&= "/$path";                                               # [154]
  342  
  343        # Date fiddling for by-{year,month,day} archive views
  344        use vars qw/ $dw $mo $mo_num $da $ti $yr $hr $min $hr12 $ampm /;  # [155]
  345        ($dw,$mo,$mo_num,$da,$ti,$yr) = nice_date($files{"$path_file"});
  346        ($hr,$min) = split /:/, $ti;
  347        ($hr12, $ampm) = $hr >= 12 ? ($hr - 12,'pm') : ($hr, 'am'); 
  348        $hr12 =~ s/^0//; $hr12 == 0 and $hr12 = 12;
  349    
  350        # Only stories from the right date
  351        my($path_info_yr,$path_info_mo_num, $path_info_da) = split /\//, $date;
                                                                               # [156]
  352        next if $path_info_yr && $yr != $path_info_yr; last if $path_info_yr &&
       $yr < $path_info_yr;                                                    # [157]
  353        next if $path_info_mo_num && $mo ne $num2month[$path_info_mo_num];
                                                                               # [158]
  354        next if $path_info_da && $da != $path_info_da; last if $path_info_da &&
       $da < $path_info_da;                                                    # [159]
  355    
  356        # Date 
  357        my $date = (&$template($path,'date',$flavour));                   # [160]
                                                                               # [161]
  358        
  359        # Plugins: Date
  360        foreach my $plugin ( @plugins ) { $plugins{$plugin} > 0 and
       $plugin->can('date') and $entries = $plugin->date($currentdir, \$date,
       $files{$path_file}, $dw,$mo,$mo_num,$da,$ti,$yr) }                      # [162]
  361    
  362        $date = &$interpolate($date);                                     # [163]
  363    
  364        $curdate ne $date and $curdate = $date and $output .= $date;      # [164]
  365        
  366        use vars qw/ $title $body $raw /;                                 # [165]
  367        if (-f "$path_file" && $fh->open("< $path_file")) {               # [166]
  368          chomp($title = <$fh>);
  369          chomp($body = join '', <$fh>);
  370          $fh->close;
  371          $raw = "$title\n$body";                                         # [167]
  372        }
  373        my $story = (&$template($path,'story',$flavour));                 # [168]
  374    
  375        # Plugins: Story
  376        foreach my $plugin ( @plugins ) { $plugins{$plugin} > 0 and
       $plugin->can('story') and $entries = $plugin->story($path, $fn, \$story,
       \$title, \$body) }                                                      # [169]
  377        
  378        if ($content_type =~ m{\Wxml$}) {                                 # [170]
  379          # Escape <, >, and &, and to produce valid RSS
  380          my %escape = ('<'=>'&lt;', '>'=>'&gt;', '&'=>'&amp;', '"'=>'&quot;');  
                                                                               # [171]
  381          my $escape_re  = join '|' => keys %escape;                      # [172]
  382          $title =~ s/($escape_re)/$escape{$1}/g;                         # [173]
  383          $body =~ s/($escape_re)/$escape{$1}/g;
  384        }
  385    
  386        $story = &$interpolate($story);                                   # [174]
  387      
  388        $output .= $story;
  389        $fh->close;                                                       # [175]
  390    
  391        $ne--;
  392      }
  393    
  394      # Foot
  395      my $foot = (&$template($currentdir,'foot',$flavour));               # [176]
  396    
  397      # Plugins: Foot
  398      foreach my $plugin ( @plugins ) { $plugins{$plugin} > 0 and
       $plugin->can('foot') and $entries = $plugin->foot($currentdir, \$foot) }
                                                                               # [177]
  399    
  400      $foot = &$interpolate($foot);                                       # [178]
  401      $output .= $foot;
  402  
  403      # Plugins: Last
  404      foreach my $plugin ( @plugins ) { $plugins{$plugin} > 0 and
       $plugin->can('last') and $entries = $plugin->last() }                   # [179]
  405  
  406    } # End skip
  407  
  408    # Finally, add the header, if any and running dynamically
  409    $static_or_dynamic eq 'dynamic' and $header and $output = header($header) .
       $output;                                                                # [180]
  410    
  411    $output;                                                              # [181]
  412  }
  413  
  414  
  415  sub nice_date {                                                         # [182]
  416    my($unixtime) = @_;                                                   # [183]
  417    
  418    my $c_time = ctime($unixtime);                                        # [184]
  419    my($dw,$mo,$da,$ti,$yr) = ( $c_time =~ /(\w{3}) +(\w{3}) +(\d{1,2})
       +(\d{2}:\d{2}):\d{2} +(\d{4})$/ );                                      # [185]
  420    $da = sprintf("%02d", $da);                                           # [186]
  421    my $mo_num = $month2num{$mo};                                         # [187]
  422    
  423    return ($dw,$mo,$mo_num,$da,$ti,$yr);
  424  }
  425  
  426  
  427  # Default HTML and RSS template bits                                    # [188]
  428  __DATA__
  429  html content_type text/html
  430  html head <html><head><link rel="alternate" type="type="application/rss+xml"
       title="RSS" href="$url/index.rss" /><title>$blog_title $path_info_da
       $path_info_mo $path_info_yr</title></head><body><center><font
       size="+3">$blog_title</font><br />$path_info_da $path_info_mo
       $path_info_yr</center><p />
  431  html story <p><a name="$fn"><b>$title</b></a><br />$body<br /><br />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>\n
  432  html date <h3>$dw, $da $mo $yr</h3>\n
  433  html foot <p /><center><a href="http://www.blosxom.com/"><img
       src="http://www.blosxom.com/images/pb_blosxom.gif" border="0"
       /></a></body></html>
  434  rss content_type text/xml
  435  rss head <?xml version="1.0"?>\n<!-- name="generator"
       content="blosxom/$version" -->\n<!DOCTYPE rss PUBLIC "-//Netscape
       Communications//DTD RSS 0.91//EN"
       "http://my.netscape.com/publish/formats/rss-0.91.dtd">\n\n<rss
       version="0.91">\n  <channel>\n    <title>$blog_title $path_info_da
       $path_info_mo $path_info_yr</title>\n    <link>$url</link>\n   
       <description>$blog_description</description>\n   
       <language>$blog_language</language>\n
  436  rss story   <item>\n    <title>$title</title>\n   
       <link>$url/$yr/$mo_num/$da#$fn</link>\n    <description>$body</description>\n 
       </item>\n
  437  rss date \n
  438  rss foot   </channel>\n</rss>
  439  error content_type text/html
  440  error head <html><body><p><font color="red">Error: 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.</font>\n\n
  441  error story <p><b>$title</b><br />$body <a
       href="$url/$yr/$mo_num/$da#fn.$default_flavour">#</a></p>\n
  442  error date <h3>$dw, $da $mo $yr</h3>\n
  443  error foot </body></html>
  444  __END__