Generation of a Drupal site map using page paths

There are several modules that allow to build a site map for Drupal. As a rule such modules are based on taxonomy/categories/book, however if you need a simple site map module that is generated using url aliases, you will find the solution below.

There are 2 main ideas for this module creation. First, the module should generate the sitemap out of path aliases. And one more thing - the module should allow modification of the generated structure. Taking into consideration our goal, we have decided to generate the menu from the aliases, which will be partially maintained by Drupal.

To create the module you should create a folder sites/all/modules/path_menu and place 2 files here. First one is the module info (cut and copy this to the path_menu.info file):

name = "Path menu"
description = "Generate the sitemenu from the path urls"
version = "6.x-1.0"
core = 6.x

And second file (path_menu.module) is the main file of the module, the content of it is the following:

<?php

function path_menu_menu() {
    $items = array();
    $items['sitemap'] = array(
        'title' => t('Path menu'),
        'page callback' => 'path_menu_page',
        'access arguments' => array('access content'),
        'type' => MENU_CALLBACK
    );
    return $items;
}

function _path_menu_delete($ndi) {
    $ip = drupal_get_normal_path($ndi->path);
    $mlid = db_result(db_query("select mlid from {menu_links} where link_path='%s' and menu_name='sitemap'", $ip));
    if ($mlid)
        db_query("delete from {menu_links} where (mlid='$mlid' or p1='$mlid' or p2='$mlid' or p3='$mlid' or p4='$mlid' or p5='$mlid' or p6='$mlid' or p7='$mlid' or p8='$mlid' or p9='$mlid') and menu_name='sitemap'");
}


function _path_menu_generate(&$ndi) {
    if (!db_result(db_query("select * from {menu_custom} where menu_name='sitemap'")))
        db_query("insert into {menu_custom} (menu_name, title, description) values('sitemap', 'Sitemap menu', '')");
//db_query("delete from menu_links where menu_name='sitemap'");

    if ($ndi->path) {
        $paths = explode('/', $ndi->path);
        $current_path = '';
        $plid = 0;
        foreach($paths as $path) {
            $current_path[] = $path;
            $ip = drupal_get_normal_path(implode('/', $current_path));
            if (!$ip) continue;
            list($_tmp, $nid) = explode('/', $ip);
            if ($nid == $ndi->nid) $node = $ndi;
            else $node = node_load($nid);
            $mlid = db_result(db_query("select mlid from {menu_links} where link_path='%s' and menu_name='sitemap'", $ip));
            if ($mlid)
                db_query($sql="update {menu_links} set link_title='".addslashes($node->title)."' where mlid='$mlid'");
            else {
                db_query("insert into {menu_links} (menu_name, plid, link_path, router_path, link_title, module, options, customized, expanded) values('%s', '%d', '%s', '%s', '%s', '%s', '%s', '%d', '%s')"
, 'sitemap', $plid, $ip, 'node/%', $node->title, 'menu', serialize(array()), 1, 1);
                $mlid = db_last_insert_id('menu_links', 'mlid');
                $query = '';
                $parents = array();
                if ($plid) {
                    $parents = db_fetch_array(db_query("select p1, p2, p3, p4, p5, p6, p7, p8, p9 from {menu_links} where mlid='%s'", $plid));
                    foreach($parents as $k=>$v) if (!$v) unset($parents[$k]);
                    $parents = array_values($parents);
                }
                $parents[] = $mlid;

                foreach($parents as $index=>$p)
                    $query[] = "p".($index+1)." = '$p'";
                $query[] = "depth = '".count($parents)."'";
                db_query($sql="update {menu_links} set ".implode(', ', $query)." where mlid='$mlid'");
                if (isset($parents[count($parents)-2]))
                    db_query("update {menu_links} set has_children=1 where mlid='%d'", $parents[count($parents)-2]);
            }
            $plid = $mlid;
        }
    }
}

function path_menu_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  switch ($op) {
    case 'delete':
        _path_menu_delete($node);
        break;
    case 'presave':
        _path_menu_generate($node);
        break;
    }
}

function path_menu_tree($tree, $level = 0) {
    $result = '';
    foreach($tree as $item) {
        $link = $item['link'];
        $result .= '<div class="sitemap-level'.$level.'">'. l($link['title'], $link['href'], $link['localized_options'])."\n";
        if (is_array($item['below'])) $result .= path_menu_tree($item['below'], $level+1)."\n";
        $result .= '</div>'."\n";
    }
    return $result;
}

function path_menu_page() {
    drupal_add_css(drupal_get_path('module', 'path_menu') .'/path_menu.css');

    drupal_set_title(check_plain(variable_get('site_map_page_title', t('Site map'))));
    $tree = menu_tree_page_data('sitemap');
    return path_menu_tree($tree);
}

As you can see it works pretty easy.

Firstly we are using the path_menu_nodeapi function to cache the save node event. This way we are able to create and maintain the Drupal menu. This menu will be the same as your urls structure. So if you have got the following pages:

about (About out site)
about/contact-us (Contact us)

The 2 menu items will be created - the first level menu item ('about') and one second level menu item ('contact-us').

Please note that you can see this “Sitemap menu” in the admin area, also please note that the structure is created only when there is no menu item with the appropriate url. If there is any, only the title of the menu item will be updated.

This way you are able to change the order, structure and visibility of the menu items in the admin area once it is created. That would be very useful, because it is not always possible to say that the url structure and the site structure match. Also you are able to add some new menu items if you need them.

With the path_menu_menu we reserve the ”/sitemap” url for our module, and we should display the content here. It is done with the path_menu_page and path_menu_tree function. We are displaying the simple tree, which will look like:

<div class="sitemap-level0"><a href="/about">About us</a>
  <div class="sitemap-level1"><a href="/about/history">Our history</a></div>
  <div class="sitemap-level1"><a href="/about/people">People</a>
    <div class="sitemap-level2"><a href="/about/people/development.html">Development team</a></div>
  </div>
  <div class="sitemap-level1"><a href="/about/contact-us">Contact info</a>
</div>

Each level is marked with its own css class. So we are able to change the displaying easily. For example the following css can be used:

.sitemap-level0 {
    float: left;
    width: 33%;
}
.sitemap-level1,
.sitemap-level2,
.sitemap-level3 {
    padding-left: 20px;
}

This css will give you 3 columns sitemap with the 20px left padding for the 1,2,3 levels.