Im slowly getting back into Drupal after much time away and beginning to pick it up and the learning curve is building back up.

So much so I always like to throw out pre-defined conventions as too restrictive while keeping the rough framework to build something bigger for what I want to do.

In my case, it's a scheduler program. But mine is in the context for a church, but could work equally well elsewhere

However the idea is that each type can have a sub type, a schedule gets applied to the type and the back end calendar generates the calendar entries for the schedule. This then gets displayed in a number of ways, from an eventually RSS feed to hook into an Android application, to a Calendar and everything else in between.

Each calendar item needs to be overwritable in that it's a generic item that may have additional certain applicable details rather than being a unique entity in its own right.

This list of types should be output with a link then to each type while I alter the page then to generate a list of next upcoming schedules for that item. Click on a date and get entries for that date. etc

It's all going pretty well until I realise I need to start referring to things by database Id instead of having a nice URL because frankly, database calendar entries don't as such have a nice URL.

One of the things I would have liked to have done is to have a URL alias against a node for my event type, eg, somecategory/sometype but then to be able to use the hook_menu system to get somecategory/sometype/% (where % could be sun, mon, tue, wed etc). Bearing in mind somecategory, sometype will be variable depending on user input, only % will have a list of specific possible - sun throu sat.

Trouble is, it doesn't look like you can do it, so I've just had to use an alias such as calendar/type/%

Any thoughts on this hook_menu issue, and any thoughts overall? 2000 lines of code into this thing and Im wondering why Im doing this (oh yeah, I like doing things my way).


2 Answers 2


I had a similar (not quite so flexible) issue.

Basically a node lives at node/NID or a url-alias path. I didnt want to write a custom hook_menu to make something so flexible as somecategory/sometype/% -- but i really think you should be able to.

I used the module view_mode_page

On the manage display page for a given content type, you can assign patterns for view modes so that they will become a page at the given path. For instance, if you want to show the teaser at a specific URL, you could do node/%/teaser. If you also use Display Suite or other modules that allow you to create new view modes, you can use these modes to display one or more fields from a node at a separate URL.

Restrictions: currently, the node path must be contained in the URL. This means view mode pages must follow the form [node path/node alias]/[view mode url]. These pages are added via hook_menu. Multiple wildcard characters can be used.

I used it to set many urls for a node of the form: 'url_pattern' => 'instructors/%/'.$val

where instructors/% IS a node alias and $val is a value from a Taxonomy Term field attached to the node. Eg, I get N-subpage urls for however many terms are in this vocabulary for each node of this content type.

I then use Display Suite to create a dynamic field for a custom view mode I associated for this node and url path; I also used Display Suite to create the custom view_mode (or maybe it was the FieldUI I forget). The dyanmic field a View that takes an argument that is the wildcard in the url.

Here's a snippet of code using the view_mode_page API:

   * Implementation of hook_view_mode_page_get_patterns() from module
   * view_mode_page.
   * This lets me define additional node urls (per NID) with a custom
   * view mode.
   * I then use this view_mode along with Display Suite and custom Fields
   * to completely alter how the node is viewed on that page, and only
   * show a View of related downloadable course files.
  function MYMODULE_view_mode_page_get_patterns($results, $content_type,  $view_mode) {
    $results2 = array();
    $defaults = array('content_type' => 'course', 'view_mode' => 'course_materials_list_view', 'show_title' => 0, 'title' => '');

    // TODO: change this to like a Taxonomy Tree lookup.
    $terms = array('activities-and-handouts', 'administrative', 'assignments', 'data-recordings-and-transcripts','lectures','quizzes-exams','readings-and-reading-handouts');
    foreach ($terms as $pos => $val) {
      $results2[$pos] = (Object) array_merge($defaults, array('url_pattern' => 'instructors/%/'.$val));
    return $results = $results2;

This basically works for me. Breadcrumbs aren't totally happy. It's NOT exactly what you're asking but might help as it's very related. Good luck.


First off, to avoid any conflict, the path for a menu handled by a module should have a part that is specific for that module; the Node module uses node as first part for its menu callbacks, and the User module uses user.
Imagine what would happen if the Node module used %title/%nid as menu path for the node pages. (%title is not a placeholder Drupal currently defines.) It would be enough the node title were user, and the node page would override a user profile, or vice versa. This doesn't actually happens because node pages have a path like node/%nid, and user profiles have a path like user/%uid. (For each user ID there would be a node with that ID; that is the reason they are not enough to disambiguate a node page from a user profile page.)

In your case, the menu path cannot be somecategory/sometype, but it should be mymodule/somecategory/sometype. (Replace mymodule with the short name of the module you are implementing.)

if somecategory, and sometype are values you could obtain from a function, you can use the approach followed by the Search module, which uses the following code in its hook_menu() implementation.

foreach (search_get_info() as $module => $search_info) {
  $path = 'search/' . $search_info['path'];
  $items[$path] = array(
    'title' => $search_info['title'], 
    'page callback' => 'search_view', 
    'page arguments' => array($module, ''), 
    'access callback' => '_search_menu_access', 
    'access arguments' => array($module), 
    'type' => MENU_LOCAL_TASK, 
    'file' => 'search.pages.inc', 
    'weight' => $module == $default_info['module'] ? -10 : 0,
  $items["$path/%menu_tail"] = array(
    'title' => $search_info['title'], 
    'load arguments' => array('%map', '%index'), 
    'page callback' => 'search_view', 
    'page arguments' => array($module, 2), 
    'access callback' => '_search_menu_access', 
    'access arguments' => array($module),
    // The default local task points to its parent, but this item points to
    // where it should so it should not be changed. 
    'type' => MENU_LOCAL_TASK, 
    'file' => 'search.pages.inc', 
    'weight' => 0,
    // These tabs are not subtabs. 
    'tab_root' => 'search/' . $default_info['path'] . '/%',
    // These tabs need to display at the same level. 
    'tab_parent' => 'search/' . $default_info['path'],

If somecategory, and sometype are completely arbitrary values entered from users in some way, then you could use the following definition for the menu callback.

  $items[mymodule/%/%/%mymodule_weekday] = array(
    'load arguments' => array(1, 2),
    'page callback' => 'mymodule_calendar_view', 
    'page arguments' => array(3), 
    'weight' => $module == $default_info['module'] ? -10 : 0,
  function mymodule_weekday_load($weekday, $category, $type) {
    // Return an array, or an object basing on the arguments.
    // Return FALSE is the arguments contain values that are not acceptable.

For a path like mymodule/my_category/my_type/sun, mymodule_weekday_load() will be called as mymodule_weekday_load('sun', 'my_category', 'my_type'). When the load function returns FALSE the user will get a 404 error page.

The string mymodule I am using here is just an example. What a module normally use is its short name, except in the case the module is extending a menu defined from another module. For example, the Book module uses book/export/%/% as path for its exporting page, but uses node/%node/outline for its page that allows to set the outline to which the node belongs.

As examples of load callbacks, you can look at node_load(), and user_load().


  • oh wow thats informative and basically what i've avoided trying to do by hand. the only issue i see with the "convention" of mymodule/my-path/foo is that it really doesnt play nice with what my clients would perceive as vanity urls. "mymodule" while useful for the programmer, is kruft for the site visitor.
    – tenken
    Nov 15, 2012 at 16:40
  • Instead of mymodule, you can use any prefix, as long as it is unique, and not used from another module. Generally, you give a more significant name to the module, such a name that suggests its functionality, and its short name can be used as path prefix. That is what is done from every Drupal module, except in the case they are extending the menu defined from another module.
    – apaderno
    Nov 15, 2012 at 16:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.