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

  1. 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.
  2. 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.
  3. 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.

2 Answers 2


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' => 'Page not found',
    'access callback' => TRUE,
    'page callback' => 'custommodule_page',
    'type' => 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) >= 2 && $parts[0] == $path_root && ctype_digit($parts[1])) {
    $id = $parts[1];
    $args = array(
      ':nid' => $id,
      ':type' => $node_type,
    if (db_query('SELECT count(nid) FROM {node} WHERE nid = :nid AND type = :type AND status = 1', $args)->fetchField()) {
      if ($alias = drupal_get_path_alias('node/'.$id)) {

function _custommodule_goto($path) {
  $options = array(
    'fragment' => '',
    'query' => NULL,
    'absolute' => FALSE,
    'alias' => FALSE,
    'prefix' => '',
    'external' => FALSE,
  drupal_goto($path, $options, 301);
  • Interesting, I didn't think of hooking into the 404 behavior. Thanks for sharing. Commented Jul 11, 2011 at 8:01
  • Tested it now, and adapted the code for my specific use case. Works great - thanks again! Commented Jul 15, 2011 at 23:18

I've taken a simpler approach by writing a Drupal 7 module which inserts an additional, title-free alias on node insert. It basically forwards /content/123 to /content/123/title (the canonical alias).

 * Implements hook_node_insert().

function custom_url_redirect_node_insert($node) {
  // Insert Content Type and Path Prefix here.
  // 'contentType' => 'pathPrefix'
  $entries = array(
    'recipe' => 'recipe',
    'blog' => 'blog',
  foreach ($entries as $contentType => $pathPrefix) {
    if ($node->type == $contentType) {
      $alias = $pathPrefix . '/' . $node->nid;
      $isAlias = drupal_lookup_path('source', $alias);
      if ($isAlias == NULL) { 
        $node->path['alias'] = $alias;

Update: Further details.

Original poster asked to replicate the StackExchange URL pattern (/question/{id}/title-name) via Drupal. It's a great pattern, because it allows better SEO and allows title-names not overwrite each other.

To do this, you will need to:

  • Install the Global Redirect module
  • Enable "Create a new alias. Leave the existing alias functioning." in Path Settings (/admin/config/search/path/settings)
  • Add pattern question/[node:nid]/[node:title] to your content type in Path Patterns (/admin/config/search/path/patterns)

It will output your content type as /question/{nid}/title-name.

The problem here, as the poster mentions, is that you cannot access your content by the unique identifier, such as /question/{nid}.

That's where this module comes in. It adds /question/{nid} as a URL alias, on every new node insert. In Drupal, it will look something like this:

Alias                   | System Path
/question/123/new-title | node/123   //updated alias, canonical
/question/123/old-title | node/123   //old alias
/question/123           | node/123   //alias added via module

Global Redirect will redirect all aliases to System Path (/node/123) and from System Path it will look for the canonical alias (/question/123/new-title).

  • You are implementing hook_node_insert - not hook_menu. Plus, how'd you tackle the node updates and redirects ?
    – AKS
    Commented May 19, 2013 at 4:17
  • I've provided more detail to the answer. Hopefully that answers. Commented May 19, 2013 at 6:38
  • Instead of overriding paths via hook_menu, it adds a path alias on node insert. Node updates do not affect this alias, as it only affects the title pattern of the path. Redirects are handled by Global Redirects, as this module only adds an alias, rather than overriding paths. Commented May 19, 2013 at 6:41
  • 2
    @AyeshK was referring to the fact the code comment said "Implements hook_menu()."
    – apaderno
    Commented May 19, 2013 at 6:44
  • Yeah, I just got that, lol! Commented May 19, 2013 at 6:45

Your Answer

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

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