Build StackExchange-style url's I'm looking for a way to build what I call 'StackExchange style' url's with Drupal 7. In my definition, that means:


A node has a path alias containing both a unique identifier and the node title. For instance, if I'm writing a story and every chapter is a node, chapter 4 (titled "The day I became a lobster") would get www.example.com/chapter/4/the-day-i-became-a-lobster.
If a user knows the chapter, but not the title, he can just go to www.example.com/chapter/4, which will redirect to www.example.com/chapter/4/the-day-i-became-a-lobster.
If the title changes (www.example.com/chapter/4/the-day-i-became-a-lobster becomes www.example.com/chapter/4/the-day-i-became-a-penguin), or if the url is mistyped (www.example.com/chapter/4/the-day-i-become-a-lobster), the old or incorrect url redirects to the new / correct url.


I want this behavior because:


In some cases, my users know a product number but not the title. This way they can easily figure out the url.
Having the title in the url is good for SEO and user experience.


I guess the best approach would be to build the complete url using Pathauto and set up a redirecting mechanism that kicks in when the path start with 'chapter/[id]' but it's not a valid path alias.

Of course I can create a custom module, implementing hook_menu for the 'chapter/%' path, lookup the correct chapter node and call drupal_goto, but maybe there are better methods. How would you do it?

PS. I posted a similar question in the issue queue of Redirect, because that module looks related and very promising.
I wrote a custom module to do this very thing. I use Pathauto to automatticly create aliases for my nodes, in the form of questions/123/title-of-node-here. The following code creates a /custom404 page. I configure my "Default 404 (not found) page" (in Site information) to custom404. The module picks the node id out of the url, checks for its existence, and then redirects to node/%nid. Finally, the Global Redirect module will take over and redirect the user to the canonical url.

/**
 * Implements hook_menu().
 */
function custommodule_menu() {
  $items = array();

  $items['custom404'] = array(
    'title' =&gt; 'Page not found',
    'access callback' =&gt; TRUE,
    'page callback' =&gt; 'custommodule_page',
    'type' =&gt; MENU_LOCAL_TASK
  );

  return $items;
}


function custommodule_page() {
  $path = _custommodule_request_path();
  $parts = explode('/', $path);

  // The following lines matche a path alias to a content type
  // map /questions/%nid/%title to the 'question' content type
  _custommodule_match_nid($parts, 'questions', 'question');  
  // map /websites/%nid/%title to the 'website' content type
  _custommodule_match_nid($parts, 'websites', 'website');

  drupal_set_title(t('Page not found'));
  return ' ';
}

/**
 * custommodule_request_path() is copied from globalredirect_request_path
 */
function _custommodule_request_path() {
  if (isset($_SERVER['REQUEST_URI'])) {
    if (isset($_REQUEST['q'])) {
      $path = $_REQUEST['q'];
    }
    else {
      // This is a request using a clean URL. Extract the path from REQUEST_URI.
      $request_path = strtok($_SERVER['REQUEST_URI'], '?');
      $base_path_len = strlen(rtrim(dirname($_SERVER['SCRIPT_NAME']), '\/'));
      // Unescape and strip $base_path prefix, leaving q without a leading slash.
      $path = substr(urldecode($request_path), $base_path_len + 1);
    }
  }
  else {
    // This is the front page.
    $path = '';
  }

  // Under certain conditions Apache's RewriteRule directive prepends the value
  // assigned to $_GET['q'] with a slash. Moreover we can always have a trailing
  // slash in place, hence we need to normalize $_GET['q'].
  $path = ltrim($path, '/');

  return $path;
}

function _custommodule_match_nid($parts, $path_root, $node_type) {
  if (count($parts) &gt;= 2 &amp;&amp; $parts[0] == $path_root &amp;&amp; ctype_digit($parts[1])) {
    $id = $parts[1];
    $args = array(
      ':nid' =&gt; $id,
      ':type' =&gt; $node_type,
    );
    if (db_query('SELECT count(nid) FROM {node} WHERE nid = :nid AND type = :type AND status = 1', $args)-&gt;fetchField()) {
      if ($alias = drupal_get_path_alias('node/'.$id)) {
        _custommodule_goto($alias);
      }
    }
  }
}

function _custommodule_goto($path) {
  $options = array(
    'fragment' =&gt; '',
    'query' =&gt; NULL,
    'absolute' =&gt; FALSE,
    'alias' =&gt; FALSE,
    'prefix' =&gt; '',
    'external' =&gt; FALSE,
  );
  unset($_GET['destination']);
  drupal_goto($path, $options, 301);
}