PREVIOUS  TABLE OF CONTENTS  NEXT 

A Dynamic Navigation Bar with mod_perl

Lincoln D. Stein

Packages Used

Apache 1.3.3:           http://www.apache.org        
mod_perl 1.16:         http://www.modperl.com     
Perl 5.004_03:          http://www.perl.com/CPAN

I admit it. I love navigation bars. I go completely green with envy whenever I browse one of those fancy Web sites with navigation bars that change color as you move the mouse over them or expand and contract a table of contents with one click.

Sometimes I think "OK, that's it. I'm going to install a navigation bar like this one right now." So I download the HTML source code for the page and have a peek. What I see always diminishes my enthusiasm substantially. Navigation bars are a lot of work! Either they're done by hand using individually crafted HTML pages, or they require a slow-loading Java applet, or most frequently, they consume several pages of JavaScript code filled with convoluted workarounds for various makes and models of browser.

One of the cardinal virtues of programming in laziness, and as a Perl programmer I have this virtue in spades. I don't want to do any hard work to create my navigation bar. I just want it to appear, automatically, when I write an HTML page and save it into my Web site's document directory. When I finally bit the bullet and got down to writing a site-wide navigation bar, I used mod_perl, the nifty embedded Perl module for Apache, to create a system that automatically adds a navigation bar to all my pages without my having to lift a finger. You'll need the Apache Web server, version 1.3.3 or higher, mod_perl version 1.16 or higher, and Perl 5.004_03 or higher to use this system.

Figure 1: Screenshot 1

Figure 1: Screenshot 1

Screenshot 1 shows a page from my laboratory's Web site with the navigation bar at the top. The bar is a single row of links, embedded inside an HTML table, running along the top and bottom of the page. Each link represents a major subdivision of the site. In this case, the subdivisions are groups of software products that I maintain, namely "Jade", "AcePerl", "Boulder", and "WWW." There's also a "Home" link for the top level page of the site. When the user selects a link, it takes him directly to the chosen section. The link then changes to red to indicate that the selected section is currently active. The link remains red for as long as the user is browsing pages contained within or beneath the section (as determined by the URL path). When the user jumps out of the section, either by selecting a link from the navigation bar or by some other means, the navigation bar updates to reflect his new position.

THE CONFIGURATION FILE

The nice feature about this system is that there's no hard-coded information anywhere the HTML pages themselves about the organization of the site or the appearance of the navigation bar. The navigation bar is added to the page using configuration information contained in a site-wide configuration file. The bar's appearance is determined by Perl code. I favor a visually simple horizontal navigation bar with text links with a few adjustments to the code; however, you could change the bar so that it displays vertically, or uses inline images as its links. It's also possible to associate different sections of the site with different navigation bars, or hide the navigation bar completely.

The configuration file is usually stored with Apache's other configuration files inside the server root directory's etc subdirectory. Below you can see the configuration I use at my site. It's a simple text file consisting of tab-delimited text. The first column contains the URLs to link to for each of the site's major sections. The second column contains the text to display for each link. Blank lines and lines beginning with the comment character are ignored.

# stein.cshl.org navigation bar
# file etc/navigation.conf
/index.html           Home
/jade/                Jade
/AcePerl/             AcePerl
/software/boulder/    BoulderIO
/software/WWW/        WWW
/linux/               Linux

Notice that you can link to either a file name, e.g. index.html, or to a directory name, e.g. /jade/. The navigation bar systems treats the two cases slightly differently when deciding whether to consider a certain section "active." The system uses prefix mapping to determine whether a page lies within a section. In the example above, any page that starts with the URL /jade will be considered to be part of the "Jade" area, and the corresponding label will be highlighted in red. However, since /index.html refers to a file rather than a partial path, only the home page itself is ever considered to be within the "Home" area.

Major sections do not have to correspond to a top level directory. For example, the "Boulder" and "WWW" sections are both subdirectories beneath "software", which doesn't have an explicit entry. The navigational system will also work with user-supported directories. For example:

/~lstein/      Lincoln's Pages

ACTIVATING THE NAVIGATION BAR

Apache itself needs to be configured to use the navigation bar system. This is done by adding the <Location> directive section shown below to one of Apache's configuration files. There are three directives in this section. The SetHandler directive tells Apache that every URL on the site is to be passed to the embedded Perl interpreter. The PerlHandler directive tells the Perl interpreter what to do with the URL when it gets it. In this case, we're telling Perl to pass the URL through the Apache::NavBar module. The last directive, PerlSetVar, sets a configuration variable named NavConf to the relative configuration file path etc/navigation.conf. The Apache::NavBar module will use NavConf to find its configuration file.

<Location />
  SetHandler perl-script
  PerlHandler Apache::NavBar
  PerlSetVar NavConf etc/navigation.conf
</Location>

In this example, the <Location> directive's path argument is /, indicating that the navigation bar system is to be applied to each and every URL served by the Web site. To apply the navigation bar to a portion of the site only, you would just modify the path accordingly. You can even apply different navigation bar configuration files to different parts of your Web site!

GENERATING THE NAVIGATION BAR

Page 15 shows the code for the navigation bar module. The file should be named NavBar.pm and stored in the Apache subdirectory of the Perl library directory. This is a slightly longer code example than you've seen in previous columns, so we'll walk through it in chunks.

package Apache::NavBar;
# file Apache/NavBar.pm
use strict;
use Apache::Constants qw(:common);
use Apache::File ();

After declaring the package, the module turns on the strict pragma, thereby avoiding the use of barewords, undeclared globals, and other sloppy programming practices. The module then brings in two helper packages. As its name implies, Apache::Constants provides various constant values that are meaningful to the Apache server. We bring in the "common" constants, and then Apache::File, which contains routines useful for manipulating files.

my %BARS = ();
my $TABLEATTS = 'WIDTH="100%" BORDER=1';
my $TABLECOLOR = '#C8FFFF';
my $ACTIVECOLOR = '#FF0000';

These lines define several file-wide lexical variables. %BARS is a hash that will be used to hold a set of "NavBar" navigation bar objects. The NavBar class, which we'll examine later, defines methods for reading and parsing navigation bar configuration files, and for returning information about a particular navigation bar. Because a site is free to define several different navigation bars, the %BARS hash is necessary to keep track of them. The hash's keys are the paths to each navigation bar's configuration file, and its values are the NavBar objects themselves.

The $TABLEATTS, $TABLECOLOR, and $ACTIVECOLOR globals control various aspects of the navigation bar's appearance. $TABLEATTS controls the table's width and border attributes, $TABLECOLOR sets the background color of each cell, and $ACTIVECOLOR sets the color of the active links.

sub handler {
    my $r = shift;

This begins the definition of the handler() subroutine, which Apache calls to fetch a requested document. The subroutine begins by shifting the request object off the subroutine stack. The request object will be used subsequently for all communication between the subroutine and Apache.

$r->content_type eq 'text/html' || return DECLINED;
my $bar = read_configuration($r) || return DECLINED;
my $table = make_bar($r, $bar);

In this section we attempt to read the configuration file and create or retrieve the appropriate navigation bar object. The first thing we do is test the requested document's MIME type by calling the content_type() method. If the MIME type is anything other than text/html, it doesn't make any sense to add a navigation bar, so we return a result code of DECLINED. This tells Apache to pass the request on to the next module that has expressed interest in processing requests. Usually this will be Apache's default document handler, which simply sends the file through unmodified.

Otherwise, we try to read the currently configured navigation bar definition file, using an internal routine named read_configuration(). If this routine succeeds, it will return the navigation bar object. Otherwise it returns undef. Again, we exit with a DECLINED error code in case of failure.

In the third line, we call an internal routine named make_bar(), which turns the NavBar object into a properly formatted HTML table.

    $r->update_mtime;
    $r->set_last_modified($bar->modified);
    my $rc = $r->meets_conditions;
    return $rc unless $rc == OK;

This bit of code represents a useful optimization. In order to reduce network usage, most modern browsers temporarily cache the files they retrieve on the user's hard disk. Then, in the request header, the browser sends the server an HTTP header called If-Modified-Since, which contains the modification time and date of the cached file. In order to avoid an unnecessary file transmission, the server should compare the modification time specified in the If-Modified- Since header to the modification time of the file on disk. If the modification time of the server's copy is the same as the time specified by the browser, then there's no reason to retransmit the document, and the server can return an HTTP_NOT_MODIFIED status code. Otherwise, the server should send the updated file.

In this case, the logic is a bit more complicated, because the contents of the requested document depend on not one but two factors: the modification time of the file, and the modification time of the navigation bar configuration file. Fortunately, Apache has a general mechanism for dealing with these situations. We begin calling the request object's update_mtime() method to copy the requested file's modification time into Apache's internal table of outgoing HTTP header fields. Next we call set_last_modified() with the modification date of the navigation bar configuration file. This updates the modification time that is sent to the browser, but only if it's more recent than the modification time of the requested file. The navigation bar's modification date is, conveniently enough, returned by the NavBar object's modified() method.

The next line calls the request object's meets_conditions() method. This checks whether the browser made a conditional request using the If-Modified-Since header field (or any of the conditional fetches defined by HTTP/1.1). The method returns OK if the document satisfies all the conditions and should be sent to the browser, or another result (usually HTTP_NOT_MODIFIED) otherwise. To implement the conditional fetch, we simply check whether the result code is OK. If not, we return the status code to Apache and it forwards the news on to the browser. Otherwise, we manufacture and transmit the page.

my $fh = Apache::File->new($r->filename)
                                || return DECLINED;

This next line attempts to open the requested file for reading, using the Apache::File class. Apache::File is an object-oriented filehandle interface. It is similar to IO::File, but has less of an impact on performance and memory footprint. The request object's filename() method returns the physical path to the file. If anything fails at this point, we return DECLINED, invoking Apache's default handling of the request. Otherwise, the filehandle object returned by Apache::File->new() is stored in a variable named $fh.

$r->send_http_header;
return OK if $r->header_only;

The send_http_header() method makes Apache send the HTTP header off to the browser. This header includes the If-Modified-Since field set earlier, along with other header fields set automatically by Apache. The second line represents yet another optimization. If the brower sent a HEAD request, then it isn't interested in getting the document body and there's no reason for this module to send it. The header_only() method returns true if the current request uses the HEAD method. If so, we return OK, telling Apache that the request was handled successfully.

local $/ = "";
while (<$fh>) {
    s:(<BODY.*?>):$1$table:soi;
    s:(</BODY>):$table$1:oi;
} continue {
    $r->print($_);
}
return OK;

These lines send the document body. We read from the HTML file paragraph by paragraph, looking for <BODY> and</BODY> tags. When we find either, we insert the HTML table containing the navigation bar adjacent to it. The bar is inserted beneath <BODY> and immediately above </BODY>. The reason for using paragraph mode and a multiline regular expression is to catch the common situation in which the <BODY> tag is spread across several lines. The regular expression isn't guaranteed to catch all possible <BODY> tags (in particular, it'll mess up on tags with an embedded '>' symbol), but it works for the vast majority of cases.

We then send the possibly modified text to the browser using the request object's print() method. After reaching the end of the file, we return OK, completing the transaction. Note that there is no need to explicitly close the Apache::File object. The filehandle is closed automatically when the object goes out of scope.

We now turn our attention to some of the utility functions used by this module, starting with make_bar():

sub make_bar {
    my ($r, $bar) = @_;
    # create the navigation bar
my $current_url = $r->uri;
my @cells;
foreach my $url ($bar->urls) {
    my $label = $bar->label($url);
    my $is_current = $current_url =~ /^$url/;
    my $cell = $is_current
? qq(<FONT COLOR="$ACTIVECOLOR"
                  CLASS="active">$label</FONT>)
    : qq(<A HREF="$url"
                  CLASS="inactive">$label</A>);
    push @cells, qq(<TD CLASS="navbar" ALIGN=CENTER
                  BGCOLOR="$TABLECOLOR">$cell</TD>\n);
    }
return qq(<TABLE CLASS="navbar"
        $TABLEATTS><TR>@cells</TR></TABLE>\n);
}

The make_bar() function takes two arguments, the request object and the previously-created NavBar object. Its job is to create an HTML table that correctly reflects the current state of the navigation bar. make_bar() begins by fetching the current URL, calling the request object's uri() method. Next, it calls the NavBar object's urls() method to fetch the list of partial URLs for the site's major areas, and iterates over them in a foreach loop.

For each URL, the function fetches its human-readable label by calling $bar->label() and determines whether the current document is part of the area. What happens next depends on whether the current document is contained within the area or not. If so, the code generates a label enclosed within a <FONT> tag with the COLOR attribute set to red and enclosed in a <B> tag. In the latter case, the code generates a hypertext link. The label or link is then pushed onto a growing array of HTML table cells. At the end of the loop, the code incorporates the table cells into a one-row table, and returns the HTML to the caller.

The next bit of code defines the read_configuration() function, which is responsible for parsing the navigation bar configuration file and returning a new NavBar object.

# read the navigation bar configuration file and
# return it as a hash.
sub read_configuration {
  my $r = shift;
  return unless my $conf_file = $r->dir_config('NavConf');
  return unless -e
  ($conf_file=$r->server_root_relative($conf_file));
  my $mod_time = (stat _)[9];
  return $BARS{$conf_file} if $BARS{$conf_file}
    && $BARS{$conf_file}->modified >= $mod_time;
return $BARS{$conf_file} = NavBar->new($conf_file);
}

The most interesting feature of read_configuration() is that it caches its results so that the configuration file is not reparsed unless it has changed recently. The function begins by calling the request object's dir_config() method to return the value of the directory configuration variable NavConf (this was previously set in the <Location> sectin with the PerlSetVar configuration directive). If no such configuration variable is defined, dir_config() returns undef and we exit immediately.

Otherwise, we call server_root_relative() to turn a relative pathname like etc/navigation.conf into an absolute one like /usr/local/apache/etc/navigation.conf. We test for the existence of the configuration file using Perl's –e switch, and then fetch the file's modification time using the stat() call. We now test the cached version of the configuration file to see if we can still use it by comparing the modification time of the configuration file with the time returned by the cached copy's modified() method. We return the cached copy of the navigation bar object if it exists and it is still fresh. Otherwise, we invoke the NavBar class's new() method to create a new object from the configuration file, store the returned object in the %BARS object cache, and return the object.

The next bit of code defines the NavBar class, which is really just an object-oriented interface to the configuration file.

package NavBar;

# create a new NavBar object
sub new {
    my ($class, $conf_file) = @_;
    my (@c, %c);
    my $fh = Apache::File->new($conf_file) || return;
    while (<$fh>) {
        chomp;
        next if /^\s*#/; # skip comments
        next unless my($url, $label) = /^(\S+)\s+(.+)/;
        push @c, $url; # keep urls in an array
        $c{$url} = $label; # keep its label in a hash
    }
    return bless { 'urls' => \@c,
                 'labels' => \%c,
        'modified' => (stat $conf_file)[9]}, $class;
}

Following the package declaration, we define the NavBar::new() method. The method takes two arguments, the class name and the path to the configuration file. The method begins by opening the configuration file, again using the Apache::File utility class to return an open file-handle. The code reads from the filehandle line by line, skipping comments and blank lines. Otherwise we parse out the section URL and its label and store them it into an array and a hash. Both are necessary because we need to keep track of both the mapping from URL to section label and the order in which to list the sections in the navigation bar. When the navigation bar has been completely read, the list of section URLs, the labels, and the modification time of the configuration file are all stored into a new object, which we return to the caller.

# return ordered list of the URLs in the bar
sub urls { return @{shift->{'urls'}}; }

# return the label for a URL in the navigation bar
sub label { return $_[0]->{'labels'} || $_[1]; }

# return the modification date of the config file
sub modified { return $_[0]->{'modified'}; }

The last three subroutines defined in this module are accessors for NavBar configuration date. urls() returns the ordered list of URLs that define the configured main sections of the site. label() takes a section URL and returns the corresponding section label. If no label is configured, it just returns the original URL. Finally, modified() returns the modification time of the configuration file, for use in caching.

A FOUNDATION TO BUILD ON

The navigation bar displayed by this module is spartan in appearance because my taste runs to simplicity. However, with a little work, it can be made as snazzy as you desire. One of the simpler ways to change the appearance of the navigation bar is to take advantage of the cascading stylesheet standard. Both the navigation bar table and the individual cells are tagged with the "navbar" style, which is currently unused. Further, the links themselves contain style tags. The links for the active, current section are tagged with the style class named "active", while the links for the other sections are tagged with "inactive." By placing a stylesheet definition in your pages, you can adjust the appearance of the table to suit your preferences.

You might wish to enhance the navigation bar by turning it into a column of labels that runs down the left hand side of the page. To do this, you'd either have to use frames, or place both the navigation bar and the HTML page into a table in two side-by-side cells. Or you could replace the links with in-line images of buttons or tabs to create a spiffy graphical navigation bar. In this case, you'd want two sets of buttons: one for the button in unpressed inactive state, and one for its pressed active state to indicate that the user is in its corresponding section.

Finally, a nice enhancement would be to add an extra dimension to the navigation bar, creating a hierarchical list of sections and subsections that expands outward to show more detail when the user enters a particular section. That'll put an end to navigation bar envy!

__END__


Lincoln Stein is the author of CGI.pm and several books on programming for the Web. His most recent is Writing Apache Modules with Perl and C, coauthored with Doug MacEachern.
PREVIOUS  TABLE OF CONTENTS  NEXT