hook_preprocess_page: "node_load() + node_page_view()" versus "%node + node_page_view()" A basic node (nid "123") exists and a module registered two paths using hook_menu:


first/%node
second




Both page callbacks use node_page_view($node) to display a fullpage view of the node,  except the second callback additionally uses node_load(123) to get a reference to $node.

Therefore http://localhost/first/123 and http://localhost/second look the same as far as the user is concerned.



However hook_preprocess_page receives the node variable only with the first path, therefore Drupal obviously does something beyond node_load when it sees a %node wildcard, any idea what it is so that hook_preprocess_page receives the reference to $node in both cases ? (because handling everything internally in the second path produces much more friendly urls than redirecting to the first path from the second).

Example:

&lt;?php

/**
 * Implementation of hook_preprocess_page().
 */
function mymodule_preprocess_page(&amp;$vars){
    if (@isset($vars['node'])) echo '&lt;h1&gt;[mymodule.module] Node is defined&lt;/h1&gt;';
    else echo '&lt;h1&gt;[mymodule.module] Node is NOT defined&lt;/h1&gt;';
}


/**
 * Implementation of hook_menu().
 */
function mymodule_menu(){
    return array(
        'first/%node' =&gt; array(
            'title' =&gt; 'First callback',
            'page callback' =&gt; 'mymodule_page_first',
            'page arguments' =&gt; array(1),
            'access callback' =&gt; TRUE, 
        ),
        'second' =&gt; array(
            'title' =&gt; 'Second callback',
            'page callback' =&gt; 'mymodule_page_second',
            'access callback' =&gt; TRUE, 
        ),
    );
}

/**
 * Page callback for url "http://localhost/first/123".
 */
function mymodule_page_first($node){
    echo '&lt;h1&gt;[mymodule.module] Function mymodule_page_first&lt;/h1&gt;&lt;pre&gt;';
    var_dump($node);
    echo '&lt;/pre&gt;';

    return node_page_view($node);
}


/**
 * Page callback for url "http://localhost/second".
 */
function mymodule_page_second(){
    $node = node_load(123); //the ID of an existing node

    echo '&lt;h1&gt;[mymodule.module] Function mymodule_page_second&lt;/h1&gt;&lt;pre&gt;';
    var_dump($node);
    echo '&lt;/pre&gt;';

    return node_page_view($node);
}


(Source of file mymodule.module)



My best/only hope so far is to dig in the source of node_page_view and all functions it consequently triggers until I find what happens between the moment node_page_view is called and the moment the module_invoke_all which triggers the hook is called.
When mymodule_preprocess_page() is invoked for the "first/%node" path, it gets the node object because the path is associated with a node from the module.
Drupal cannot associate a node object with a path like "/admin/content/node" (Drupal 6 path) because the path doesn't contain any reference to a node ID which is marked as node ID. Which node object should Drupal return for such paths, considering a Drupal website can contains hundreds of nodes?

Your question is "how can Drupal associate a node object in one case, but not in the other one?"

The answer is in the template_preprocess_page() code, which the function executed before the template page.tpl.php is rendered.
In that function, you will find the following code:

if ($node = menu_get_object()) {
  $variables['node'] = $node;
}


The default parameters for menu_get_object() are $type = 'node', $position = 1, $path = NULL, which means template_preprocess_page() is asking for the node object that is associated with the placeholder in position #1, which is the value returned, for example, from arg(1).

Looking at the source of menu_get_object(), you will notice that the code being executed is the following:

function menu_get_object($type = 'node', $position = 1, $path = NULL) {
  $router_item = menu_get_item($path);
  if (isset($router_item['load_functions'][$position]) &amp;&amp; !empty($router_item['map'][$position]) &amp;&amp; $router_item['load_functions'][$position] == $type . '_load') {
    return $router_item['map'][$position];
  }
}


Using the default arguments, the function will verify the loading function associated with the route item #1 is node_load(). If the loading function is that, then it will return the value returned by the loading function to the calling function (in our case, template_preprocess_node()).

To automatically get a node object, your menu callback should use a path like "second/%node" (which would be compatible with the default menu_get_object() arguments). The only problem is that for such paths the node ID needs to be passed; if the URL is http://example.com/second, Drupal will show a 404 error page, as the placeholder require parameter that must be passed.
To resolve this problem, the module should define two menu callbacks: one associated with "second", and one associated with "second/%node". The first menu callback would not get a node object, while the second would get the node object (and mymodule_preprocess_page() would get it too).

As alternative, you should change the code of mymodule_preprocess_page() to something similar to:

/**
 * Implementation of hook_preprocess_page().
 */
function mymodule_preprocess_page(&amp;$vars){
  // Check if the page being served is one of the pages handled by the module.
  if (arg(0) == 'first' || arg(0) == 'second') {
    if (isset($vars['node'])) {
      // The page is a node page.
    }
    else {
      $vars['node'] = mymodule_load_the_default_node();
    }
    // …
  }
}