Skip to page content or skip to Accesskey List.
Search evolt.org
evolt.org login: or register

Work

Main Page Content

How to create a hierarchical navigation menu

Rated 3.49 (Ratings: 2) (Add your rating)

Log in to add a comment
(5 comments so far)

Want more?

 
Picture of Martin Tsachev

Martin Tsachev

Member info | Full bio

User since: June 26, 2001

Last login: March 29, 2006

Articles written: 6

Introduction

Want to create a hierarchical navigation menu for your site — you think that's hard, read on and I'll show you it's not. The great thing about it is the section you're in will be expanded while the others collapsed to represent relation of the pages.

Why would one want hierarchical navigation links ah? — well this really improves organisation as a whole, eases finding of information and access to related information.

What's required

First of all your site will have to use a hierarchical structure or have some special way of representing it, using canonical URIs is recommended, you won't get the result wanted otherwise.

My site uses uses some remapping techniques so that more friendly URIs are presented to the user( and robots of course). If you want to know more about that a good start is to read Search Engine Friendly URLs, URLs! URLs! URLs! or How to Succeed with URLs. Apache's documentation also has many examples of the use of mod_rewrite.

I'm using PHP for the code but there's no problem to use any given language that's used embedded in HTML or as a CGI, just some little adjustments need to be done.

Configuration

A good starting point is adding some configuration to your .htaccess or httpd.conf file.

DirectoryIndex site index.php
<Files "site">
	ForceType application/x-httpd-php
</Files>

DirectoryIndex sets the file to be served when Apache receives a request for a directory. The next declaration forces the "site" file (without the quotes) to be treated as PHP code, even though it doesn't have the php extension.

Action handle_my_php_pages /shaggy/site
AddHandlet handle_my_php_pages .php

These two lines can ensure that your global page handler will also be called when a direct request for a php file is made. This is optional and may require some modifications to the code.

Style rules to use

ul.links {
	margin-left : 0;
	padding-left : 0;
}

ul.links li {
        margin-left : 0;
        list-style : none;
}

ul.links li ul {
	padding-left : 2em;
	margin-left : 0;
}

This will prevent the default rendering of the unordered list items that browsers do and change it into something that's more likely to look appropriate for navigation links.

The core of it

I have all PHP code that is shown below saved in a file named site.

$pages = array(
	'Home' => 'home',
	'News' => array(
		0 => 'news/',
		'Europe' => 'news/europe',
		'USA' => 'news/usa',
		'Asia' => 'news/asia'),
	'Contact' => 'contact');
$root = 'shaggy';
$handler = 'site';

The pages array will hold the labels and their mapping to the URIs. Note the index value 0 used within the News subcategory, it maps to the section index. I used 0 because it evaluates to false in PHP, you will see why a bit later.

The other two variables are set to the root of your site, and to the page that will handle file includes.

$params = explode('/', $HTTP_SERVER_VARS['REQUEST_URI']);
array_shift($params);
foreach ( $params as $i => $value ) {
	if ( $value == $root or $value == $handler )
		array_shift($params);
}

$include_file = implode('/', $params);
if ( sizeof($params) < 2 ) {
	$include_file = 'root/' . $include_file;
	$params[0] = 'root';
}

$include_file = explode('?', $include_file);
$include_file = $include_file[0];

if ( $params[sizeof($params)-1] == '' ) {
	$include_file .= 'index';
}

$include_file .= '.php';

if ( ! file_exists($include_file) ) {
	$include_file = "$params[0]/index.php";
}
include($include_file);

First explode the URI on /es ( track_vars must be set to on in php.ini or a .htaccess file, else just use $REQUEST_URI) and get rid of the first empty value — that's because webservers process only absolute requests and they start with a slash. Next remove the other unnecessary info — namely the root and the global page handler for your site.

$include_file = implode('/', $params) does the same thing backwards, this time without the site global info to get the filename to be included. A check is performed to find if the request is for a page in no category, I have all of them in a directory called root. You're maybe wondering why I have $params[0] = 'root' — this is because of the file_exist later on. Next remove the query string if present, that is usually done by the webserver for you but this time you want to include the files yourself and that's needed.

Directory indexed end with "/"(a slash) so append index if this is the case, otherwise you will have to store your directory indexes in files called .php and that may be quite confusing.

Finally, perform a check if the file exists, if not forward to the section index (this code may need some improvements if you think this is likely to happen often). include the file that's requested.

The create_nav_links does the hard work, it will traverse a given array and display the appropriate links, if it finds another array it will call itself with it as a parameter.

function create_nav_links( $section, $level ) {
	global $params;
	if ( ! $level ) {
		$markup = '<ul class="links">' . "\n";
	} else {
		$markup = "<ul>\n";
	}
	foreach ( $section as $desc => $filename ) {
		if ( ! is_array($filename) ) {
			if ( $desc )
				$markup .= '<li><a href="' . $handler . '/' . $filename . '">' . $desc . "</a></li>\n";
		} else {
			if ( $params[$level] . '/' == $filename[0] ) {
				$markup .= '<li><a href="' . $handler . '/' . $filename[0] . '">' . $desc . "</a>\n";
				$markup .= create_nav_links($filename, $level +1);
				$markup .= "</li>\n";
			} else {
				$markup .= '<li><a href="' . $handler . '/' . $filename[0] . '">' . $desc . "</a></li>\n";
			}
		}
	}
	return ( $markup . "</ul>\n" );
}

echo create_nav_links($pages, 0);

First check if we are at level 0, the topmost, if so add a class of links to the unordered list (see the style sheet given above). All new lines are optional and are added just to make the output HTML look nicer.

The man loop traverses the array passed as a parameter, first we check to see if we're dealing with a normal file — i.e. not a section (which is represented as an array in our pages structure. Then a simple check if we have a description for this element — remember the 0 index used for the section URI in pages. If this was not checked you would have end up with categories listed again within themselves.

In the other case — a category check (if ( $params[$level] . '/' == $filename[0] ) if this is what we're within, we don't want to mess up the navigation menu with tons of links. If it is indeed our category, display a link to the index and add the output of create_nav_links, this time called with the section array and a level deeper.

If this is some other category, the visitor is probably not interested in, just display a links to its index — $markup .= '&lt;li&gt;&lt;a href="' . $handler . '/' . $filename[0] . '"&gt;' . $desc . "&lt;/a&gt;&lt;/li&gt;\n"

Finally echo out the navigation to the browser, you should take care that it is printed where appropriate.

A little something that has to be added to the head of your pages:

<?php
$base = 'http://' . $HTTP_SERVER_VARS['SERVER_NAME'] . $HTTP_SERVER_VARS['SCRIPT_NAME'];
?>
<base href="<?php echo $base?>" />

That ensures the consistency of your links generated by the create_nav_links function. Again this one requires track_vars = on in PHP's configuration.

What else

You noticed that last note about the links, right? Well, it ensures that these links are okay but when you begin using anchors in your documents you can't use anchor names relative to the current page, you'll have to make them relative to your document root.

You smarty, why don't you fix your function — well it will be not a problem to fix this function but what about any global style sheets or images referenced from your pages — you'll have to modify them for every page.

A better solution to this problem would be to use a function for creating links that are absolute (for the site) and get rid of the base href. Such a function would take as a parameter for example news/europe and return /shaggy/site/news/europe, the $root and $handler variables can be used for this transformation.

This way to hold the navigation menu, as an PHP array, is good only if you want to update it into the source everytime you change something. A better solution would be to use a database to hold that data, even you may want to get the contents of the pages from a database source.

Martin Tsachev started using computers in 1992, programming Basic and has since then developed a great passion for them.

Nowadays he runs mtdev - a web site with highlight on PHP.

Less than accessible

Submitted by CliveSweeney on March 9, 2002 - 10:28.

This article is really of no use to anyone who has no idea what is meant by terms like "canonical URIs" or "htaccess or httpd.conf file". I wonder how many people start to read it because it seems like it would be useful and then are turned off after just a few sentences. Then again, I haven't rated the article because I'm sure it might be useful to some people.

login or register to post comments

look at the links above

Submitted by Martin Tsachev on March 9, 2002 - 23:57.

I have provided some links to articles that describe what are friendly URLs, you should always try to have canonical URLs anyway but having friendly URLs is one way to get to that. This article is by no means a tutorial on canonical URLs or how to set up a webserver. There're other articles that deal with that issues and my should have been probably a lot bigger and unreadable if it had all that.

login or register to post comments

Re: Less than accessible

Submitted by Spyder on March 10, 2002 - 21:50.

while you have some point, not all articles can or should be at a very low level. I've done enough reading to be able to understand this article but If I didn't know about certain terms, I would go and ask my friend google.com ;) Nice work shaggy

login or register to post comments

database backed solution

Submitted by Martin Tsachev on April 30, 2002 - 19:30.

If you need a similar functionality through a database though( save your pages info there not in a PHP array) you can get the code from my website.

Note: As we're dealing with a database this time, you also get the benefit to keep in it the pages' titles, descriptions, keywords and so on.

login or register to post comments

Appearance

Submitted by jack456 on February 24, 2005 - 07:12.

It is always to good idea to have a demo or sample link to show how the result of the script will look like.

login or register to post comments

The access keys for this page are: ALT (Control on a Mac) plus:

evolt.orgEvolt.org is an all-volunteer resource for web developers made up of a discussion list, a browser archive, and member-submitted articles. This article is the property of its author, please do not redistribute or use elsewhere without checking with the author.