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.


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


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']);


foreach ( $params as $i => $value ) {

if ( $value == $root or $value == $handler )



$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";



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">' . "


} else {

$markup = "<ul>



foreach ( $section as $desc => $filename ) {

if ( ! is_array($filename) ) {

if ( $desc )

$markup .= '<li><a href="' . $handler . '/' . $filename . '">' . $desc . "</a></li>


} else {

if ( $params[$level] . '/' == $filename[0] ) {

$markup .= '<li><a href="' . $handler . '/' . $filename[0] . '">' . $desc . "</a>


$markup .= create_nav_links($filename, $level +1);

$markup .= "</li>


} else {

$markup .= '<li><a href="' . $handler . '/' . $filename[0] . '">' . $desc . "</a></li>





return ( $markup . "</ul>

" );


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 .= '<li><a href="' . $handler . '/' . $filename[0] . '">' . $desc . "</a></li>


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:




<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.